SaylorTwift HF Staff commited on
Commit
0298809
·
verified ·
1 Parent(s): af7d04e

Add per-checkpoint eval worker (eval_watch.py + run_eval_watch.sh); ONLY_MODEL mode in eval_adapter.py

Browse files
Files changed (3) hide show
  1. eval_adapter.py +6 -1
  2. eval_watch.py +110 -0
  3. run_eval_watch.sh +78 -0
eval_adapter.py CHANGED
@@ -17,6 +17,7 @@ Config via env:
17
  N_TASKS how many test tasks (default 50)
18
  CONCURRENCY parallel rollouts (default 8; keep <= MAX_CONCURRENT_ENVS)
19
  OUT results JSON path (default /app/eval_results.json)
 
20
  """
21
 
22
  import json
@@ -31,6 +32,7 @@ VLLM_URL = os.environ.get("VLLM_URL", "http://127.0.0.1:8001")
31
  TEST_SPLIT = os.environ.get("TEST_SPLIT", "/app/prepared/datasets/test")
32
  BASE_MODEL = os.environ.get("BASE_MODEL", "LiquidAI/LFM2.5-2.6B")
33
  ADAPTER_NAME = os.environ.get("ADAPTER_NAME", "trained")
 
34
  N_TASKS = int(os.environ.get("N_TASKS", "50"))
35
  CONCURRENCY = int(os.environ.get("CONCURRENCY", "8"))
36
  OUT = os.environ.get("OUT", "/app/eval_results.json")
@@ -69,7 +71,10 @@ def run_one(task_index, model):
69
 
70
 
71
  def main():
72
- models = [BASE_MODEL] + ([ADAPTER_NAME] if ADAPTER_NAME else [])
 
 
 
73
  work = [(i, m) for i in range(N_TASKS) for m in models]
74
  cells = {} # (task_index, model) -> {"correct": bool|None, "failed": bool, "error": str|None}
75
  with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
 
17
  N_TASKS how many test tasks (default 50)
18
  CONCURRENCY parallel rollouts (default 8; keep <= MAX_CONCURRENT_ENVS)
19
  OUT results JSON path (default /app/eval_results.json)
20
+ ONLY_MODEL evaluate just this one served model name (per-checkpoint eval)
21
  """
22
 
23
  import json
 
32
  TEST_SPLIT = os.environ.get("TEST_SPLIT", "/app/prepared/datasets/test")
33
  BASE_MODEL = os.environ.get("BASE_MODEL", "LiquidAI/LFM2.5-2.6B")
34
  ADAPTER_NAME = os.environ.get("ADAPTER_NAME", "trained")
35
+ ONLY_MODEL = os.environ.get("ONLY_MODEL", "")
36
  N_TASKS = int(os.environ.get("N_TASKS", "50"))
37
  CONCURRENCY = int(os.environ.get("CONCURRENCY", "8"))
38
  OUT = os.environ.get("OUT", "/app/eval_results.json")
 
71
 
72
 
73
  def main():
74
+ if ONLY_MODEL:
75
+ models = [ONLY_MODEL]
76
+ else:
77
+ models = [BASE_MODEL] + ([ADAPTER_NAME] if ADAPTER_NAME else [])
78
  work = [(i, m) for i in range(N_TASKS) for m in models]
79
  cells = {} # (task_index, model) -> {"correct": bool|None, "failed": bool, "error": str|None}
80
  with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
eval_watch.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Per-checkpoint eval worker: watch the adapter repo, eval each new checkpoint.
2
+
3
+ Loop: poll ADAPTER_REPO for checkpoint-N dirs without eval results, hot-load each
4
+ as a LoRA into the already-running vLLM (runtime LoRA updates — no server restart),
5
+ run the held-out eval on a fixed task subset, and upload per-checkpoint results
6
+ plus a rolling eval/summary.json (the live learning curve).
7
+
8
+ Base-model cells are computed once and reused for every checkpoint.
9
+
10
+ Env: ADAPTER_REPO (required), BASE_MODEL, N_TASKS (default 100), POLL_S (600).
11
+ Stops when eval/final_results.json appears (the training job's own final eval).
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import subprocess
17
+ import time
18
+ import urllib.request
19
+ from pathlib import Path
20
+
21
+ from huggingface_hub import HfApi, snapshot_download
22
+
23
+ REPO = os.environ["ADAPTER_REPO"]
24
+ BASE_MODEL = os.environ.get("BASE_MODEL", "LiquidAI/LFM2.5-2.6B")
25
+ POLL_S = int(os.environ.get("POLL_S", "600"))
26
+ VLLM = "http://127.0.0.1:8001"
27
+
28
+ api = HfApi()
29
+
30
+
31
+ def repo_files():
32
+ return set(api.list_repo_files(REPO))
33
+
34
+
35
+ def pending_checkpoints(fs):
36
+ ckpts = sorted(
37
+ {f.split("/")[0] for f in fs if f.startswith("checkpoint-") and f.endswith("adapter_config.json")},
38
+ key=lambda s: int(s.rsplit("-", 1)[1]),
39
+ )
40
+ done = {f[len("eval/"):-len("_results.json")] for f in fs if f.startswith("eval/") and f.endswith("_results.json")}
41
+ return [c for c in ckpts if c not in done]
42
+
43
+
44
+ def run_eval(model_name, out_path):
45
+ env = dict(os.environ, ONLY_MODEL=model_name, OUT=out_path)
46
+ subprocess.run(["python3", "/app/eval_adapter.py"], env=env, check=True)
47
+
48
+
49
+ def upload(local, remote):
50
+ api.upload_file(path_or_fileobj=str(local), path_in_repo=remote, repo_id=REPO)
51
+
52
+
53
+ def lora_rpc(route, name, path=None):
54
+ payload = {"lora_name": name}
55
+ if path is not None:
56
+ payload["lora_path"] = str(path)
57
+ req = urllib.request.Request(
58
+ f"{VLLM}/v1/{route}",
59
+ data=json.dumps(payload).encode(),
60
+ headers={"Content-Type": "application/json"},
61
+ )
62
+ urllib.request.urlopen(req, timeout=900)
63
+
64
+
65
+ def pass_at_1(cells_path):
66
+ cells = json.loads(Path(cells_path).read_text())["cells"]
67
+ scored = [c["correct"] for c in cells.values() if not c["failed"]]
68
+ return (sum(scored) / len(scored)) if scored else None, len(scored)
69
+
70
+
71
+ def main():
72
+ fs = repo_files()
73
+ # 1. Base-model cells, once. Every checkpoint compares against these.
74
+ if "eval/base_cells.json" not in fs:
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
+
79
+ summary = {}
80
+ if "eval/summary.json" in fs:
81
+ summary = json.loads(Path(api.hf_hub_download(REPO, "eval/summary.json")).read_text())
82
+
83
+ loaded = []
84
+ while True:
85
+ fs = repo_files()
86
+ if "eval/final_results.json" in fs:
87
+ print("[watch] training job's final eval landed; stopping", flush=True)
88
+ return
89
+ for ckpt in pending_checkpoints(fs):
90
+ print(f"[watch] new checkpoint: {ckpt}", flush=True)
91
+ local = Path(snapshot_download(REPO, allow_patterns=[f"{ckpt}/*"])) / ckpt
92
+ lora_rpc("load_lora_adapter", ckpt, local)
93
+ loaded.append(ckpt)
94
+ if len(loaded) > 4: # stay under --max-loras
95
+ lora_rpc("unload_lora_adapter", loaded.pop(0))
96
+ run_eval(ckpt, f"/tmp/{ckpt}_cells.json")
97
+ upload(f"/tmp/{ckpt}_cells.json", f"eval/{ckpt}_results.json")
98
+ p1, n = pass_at_1(f"/tmp/{ckpt}_cells.json")
99
+ pb, nb = pass_at_1("/tmp/base_cells.json")
100
+ summary[ckpt] = {"pass_at_1": p1, "scorable": n,
101
+ "base_pass_at_1": pb,
102
+ "delta": (p1 - pb) if p1 is not None and pb is not None else None}
103
+ Path("/tmp/summary.json").write_text(json.dumps(summary, indent=2))
104
+ upload("/tmp/summary.json", "eval/summary.json")
105
+ print(f"[watch] {ckpt}: pass@1={p1} (base {pb})", flush=True)
106
+ time.sleep(POLL_S)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
run_eval_watch.sh ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Per-checkpoint eval worker (1 GPU). Polls ADAPTER_REPO for new checkpoint-N
3
+ # dirs and evaluates each on a fixed N_TASKS subset of the held-out test split,
4
+ # writing eval/<checkpoint-N>_results.json + a rolling eval/summary.json.
5
+ #
6
+ # Expected env:
7
+ # HF_TOKEN (secret) hub access + sandbox Jobs
8
+ # ADAPTER_REPO (env) repo the training job pushes checkpoints to
9
+ # N_TASKS (env) test tasks per eval (default 100; same subset every time)
10
+ set -euo pipefail
11
+ cd "$(dirname "$0")"
12
+
13
+ echo "=== [setup] ==="
14
+ apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates
15
+ python3 -m pip install --quiet \
16
+ 'openenv[harbor]' \
17
+ 'harbor[hf-sandbox]>=0.24.0' \
18
+ math-verify tomli-w \
19
+ aiohttp
20
+ git clone --depth 1 https://github.com/huggingface/OpenEnv.git /opt/OpenEnv
21
+ SITE=$(python3 -c "import site; print(site.getsitepackages()[0])")
22
+ echo "/opt/OpenEnv/envs" > "$SITE/openenv_examples.pth"
23
+
24
+ echo "=== [patch] opencode seam: session id as x-api-key (Jobs proxy strips Authorization) ==="
25
+ python3 - <<'EOF'
26
+ import openenv.harbor.seams as seams
27
+
28
+ path = seams.__file__
29
+ src = open(path).read()
30
+ old = '"apiKey": session,\n "timeout": 600_000,'
31
+ new = ('"apiKey": session,\n'
32
+ ' "headers": {"x-api-key": session},\n'
33
+ ' "timeout": 600_000,')
34
+ assert src.count(old) == 1, f"opencode seam not found in {path}"
35
+ open(path, "w").write(src.replace(old, new))
36
+ print("patched", path)
37
+ EOF
38
+
39
+ echo "=== [setup] pinned test tasks ==="
40
+ python3 prepare_tasks.py --output /app/prepared --splits test
41
+
42
+ echo "=== [start] vLLM (base; LoRAs hot-loaded per checkpoint) ==="
43
+ CUDA_VISIBLE_DEVICES=0 VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 \
44
+ vllm serve "${BASE_MODEL:-LiquidAI/LFM2.5-2.6B}" \
45
+ --port 8001 \
46
+ --max-model-len 32768 \
47
+ --return-tokens-as-token-ids \
48
+ --logprobs-mode processed_logprobs \
49
+ --enable-auto-tool-choice \
50
+ --tool-call-parser lfm2 \
51
+ --enable-lora --max-lora-rank 32 --max-loras 8 &
52
+ VLLM_PID=$!
53
+
54
+ echo "=== [start] env server (test split; tunnel-free via Jobs proxy) ==="
55
+ export SPACE_HOST="${JOB_ID}--8000.hf.jobs"
56
+ PREPARED=/app/prepared OPENENV_DATASETS=/app/prepared/datasets/test bash serve_env.sh &
57
+ ENV_PID=$!
58
+
59
+ python3 - <<'EOF'
60
+ import time, urllib.request
61
+ for name, url in [("vllm", "http://127.0.0.1:8001/v1/models"),
62
+ ("env", "http://127.0.0.1:8000/health")]:
63
+ for i in range(360):
64
+ try:
65
+ urllib.request.urlopen(url, timeout=5)
66
+ print(name, "ready")
67
+ break
68
+ except Exception:
69
+ time.sleep(5)
70
+ else:
71
+ raise SystemExit(f"{name} server never became ready")
72
+ EOF
73
+
74
+ echo "=== [watch] evaluating every new checkpoint in ${ADAPTER_REPO} ==="
75
+ python3 eval_watch.py
76
+
77
+ kill $VLLM_PID $ENV_PID 2>/dev/null || true
78
+ echo "=== done ==="