thegovind's picture
Publish the weight-space programming labs (part 2)
84bcfb1 verified
Raw History Blame Contribute Delete
6.4 kB
#!/usr/bin/env python
"""Run a plan of generation jobs on a pool of GPUs, one vLLM worker per GPU.
python scripts/run_jobs.py plans/trainer.json --gpus 0-7
A plan lists runs: {"name", "model", "data", "n", "shards", "est" (millions of tokens, for ordering),
optional "priority" (higher starts first), "preset", "set" and "args"}. "model" is "base", a folder name under
models/ ("qwen35-p50c") or a path; "data" is a file name under data/ without .jsonl. Each run is split into
shards; the biggest shards start first. Finished shards (a .done.json) are skipped, so the script can be restarted
any time. When every shard of a run is done, the run is merged into runs/<name>/merged.jsonl and summarized in
summary.json.
"""
import argparse
import fnmatch
import json
import os
import subprocess
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
from wsp.summary import summarize # noqa: E402
ap = argparse.ArgumentParser()
ap.add_argument("plan")
ap.add_argument("--gpus", default="0-7")
ap.add_argument("--lab", default="/nvme/wsp-lab")
ap.add_argument("--only", nargs="*", help="run names or globs")
ap.add_argument("--retries", type=int, default=1)
ap.add_argument("--dry", action="store_true")
a = ap.parse_args()
if "-" in a.gpus:
lo, hi = map(int, a.gpus.split("-"))
GPUS = list(range(lo, hi + 1))
else:
GPUS = [int(g) for g in a.gpus.split(",")]
plan = json.load(open(a.plan))
runs = [r for r in plan["runs"] if not a.only or any(fnmatch.fnmatch(r["name"], p) for p in a.only)]
def model_path(m):
if m == "base":
return os.path.join(a.lab, "models", "Qwen3.5-0.8B")
return m if m.startswith("/") else os.path.join(a.lab, "models", m)
def run_dir(r):
return os.path.join(a.lab, "runs", r["name"])
def shard_out(r, i):
return os.path.join(run_dir(r), f"shard{i}of{r['shards']}")
def merge(r):
rows = [json.loads(line) for line in open(os.path.join(a.lab, "data", r["data"] + ".jsonl"))]
recs, done = {}, []
for i in range(r["shards"]):
for line in open(shard_out(r, i) + ".jsonl"):
x = json.loads(line)
recs[x["id"]] = x
done.append(json.load(open(shard_out(r, i) + ".done.json")))
missing = [x["id"] for x in rows if x["id"] not in recs]
if missing and "--limit" not in r.get("args", []): # with --limit, only the first rows of each shard ran
raise KeyError(f"{len(missing)} problems have no answers, the first is {missing[0]}")
merged = [recs[x["id"]] for x in rows if x["id"] in recs]
with open(os.path.join(run_dir(r), "merged.jsonl"), "w") as f:
for x in merged:
f.write(json.dumps(x) + "\n")
s = summarize(merged)
s["run"] = r
s["sampling"] = done[0]["sampling"]
s["edit_note"] = done[0]["edit_note"]
s["tokens_total"] = sum(d["tokens"] for d in done)
s["gpu_seconds"] = round(sum(d["gen_s"] + d["load_s"] + d["grade_s"] for d in done))
s["tok_per_s_per_gpu"] = round(sum(d["tokens"] for d in done) / max(sum(d["gen_s"] for d in done), 1e-9))
json.dump(s, open(os.path.join(run_dir(r), "summary.json"), "w"), indent=1)
print(f"[{time.strftime('%H:%M:%S')}] MERGED {r['name']}: acc={s['acc']:.3f} cut={s['cut_share']:.3f} "
f"mean_tokens={s['mean_tokens']:.0f} tok/s/gpu={s['tok_per_s_per_gpu']}", flush=True)
units = []
for r in runs:
for i in range(r["shards"]):
if not os.path.exists(shard_out(r, i) + ".done.json"):
units.append((r, i))
units.sort(key=lambda u: (-u[0].get("priority", 0), -u[0].get("est", 1) / u[0]["shards"]))
print(f"{len(units)} shards to run on GPUs {GPUS}:", flush=True)
for r, i in units:
print(f" {r['name']} {i}/{r['shards']} ~{r.get('est', 0) / r['shards']:.1f}M tokens", flush=True)
if a.dry:
sys.exit(0)
L = a.lab
env = dict(os.environ, NLTK_DATA=f"{L}/nltk_data", HF_HOME=f"{L}/hf", VLLM_CACHE_ROOT=f"{L}/vllm-cache",
FLASHINFER_WORKSPACE_BASE=L, TRITON_CACHE_DIR=f"{L}/triton-cache",
TORCHINDUCTOR_CACHE_DIR=f"{L}/inductor-cache", XDG_CACHE_HOME=f"{L}/xdg-cache",
MPLCONFIGDIR=f"{L}/mpl", TOKENIZERS_PARALLELISM="false", PYTHONUNBUFFERED="1")
free, running, tries = list(GPUS), {}, {}
failed = []
def launch(r, i, g):
os.makedirs(run_dir(r), exist_ok=True)
cmd = [sys.executable, os.path.join(ROOT, "scripts", "gen_worker.py"), "--model", model_path(r["model"]),
"--data", os.path.join(a.lab, "data", r["data"] + ".jsonl"), "--out", shard_out(r, i),
"--n", str(r["n"]), "--shard", f"{i}/{r['shards']}", "--preset", r.get("preset", "think35")]
for kv in r.get("set", []):
cmd += ["--set", kv]
cmd += r.get("args", [])
log = open(shard_out(r, i) + ".log", "a")
p = subprocess.Popen(cmd, env=dict(env, CUDA_VISIBLE_DEVICES=str(g)), stdout=log, stderr=subprocess.STDOUT)
running[p] = (r, i, g, time.time())
print(f"[{time.strftime('%H:%M:%S')}] START {r['name']} {i}/{r['shards']} on GPU {g}", flush=True)
while units or running:
while units and free:
r, i = units.pop(0)
launch(r, i, free.pop(0))
time.sleep(10)
for p in list(running):
if p.poll() is None:
continue
r, i, g, t0 = running.pop(p)
free.append(g)
ok = p.returncode == 0 and os.path.exists(shard_out(r, i) + ".done.json")
mins = (time.time() - t0) / 60
print(f"[{time.strftime('%H:%M:%S')}] {'DONE' if ok else 'FAIL'} {r['name']} {i}/{r['shards']} "
f"on GPU {g} after {mins:.1f} min (exit {p.returncode})", flush=True)
if not ok:
k = (r["name"], i)
tries[k] = tries.get(k, 0) + 1
if tries[k] <= a.retries:
units.insert(0, (r, i))
else:
failed.append(k)
continue
if all(os.path.exists(shard_out(r, j) + ".done.json") for j in range(r["shards"])):
try:
merge(r)
except Exception as e: # noqa: BLE001
print("MERGE FAILED", r["name"], repr(e), flush=True)
for r in runs:
if all(os.path.exists(shard_out(r, j) + ".done.json") for j in range(r["shards"])) and \
not os.path.exists(os.path.join(run_dir(r), "summary.json")):
merge(r)
print("ALL DONE" if not failed else f"FINISHED WITH FAILURES: {failed}", flush=True)