File size: 6,403 Bytes
84bcfb1 | 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 | #!/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)
|