Download code/modal_app/beam_selfhost.py from ProCreations/Dory-Beam-4B: direct link, hf CLI and curl.
- Browser
- Download file 16.5 kB
-
https://huggingface.co/ProCreations/Dory-Beam-4B/resolve/main/code/modal_app/beam_selfhost.py
- Command line
-
hf download hf://ProCreations/Dory-Beam-4B/code/modal_app/beam_selfhost.py
-
curl -L -o beam_selfhost.py https://huggingface.co/ProCreations/Dory-Beam-4B/resolve/main/code/modal_app/beam_selfhost.py
16.5 kB
| """Self-host Beam-1.0 (NVFP4) on Modal with SGLang to generate distillation traces. | |
| modal run modal_app/beam_selfhost.py::download # one-time: weights -> Volume (CPU only) | |
| modal run modal_app/beam_selfhost.py::bench --ids FILE # throughput test + parity on 2x B300 | |
| Every generation is appended to the `beam-dory-data` Volume under selfhost/, in the | |
| same record format as scripts/generate.py, so build_dataset.py can verify and merge it. | |
| """ | |
| import json | |
| import os | |
| import subprocess | |
| import time | |
| from pathlib import Path | |
| import modal | |
| APP = modal.App("beam-dory-selfhost") | |
| weights = modal.Volume.from_name("beam-weights", create_if_missing=True, version=2) | |
| data = modal.Volume.from_name("beam-dory-data", create_if_missing=True, version=2) | |
| BEAM_REPO = "reflection/Beam-1.0-501B-A23B-NVFP4" | |
| FP8_REPO = "reflection/Beam-1.0-501B-A23B-FP8" | |
| DRAFT_REPO = "reflection/Beam-1.0-501B-A23B-DFlash2" | |
| MODELS = {"nvfp4": ("beam-nvfp4", 2), "fp8": ("beam-fp8", 4)} # weights subdir, tensor-parallel GPUs | |
| # GPU configs that fit each checkpoint, in order of preference (cost-effectiveness) | |
| GPU_CHOICES = {"nvfp4": ["B300:2"], "fp8": ["B300:4", "H200:8", "B200:8"], | |
| "auto": ["B300:4", "H200:8", "B200:8", "B200:4", "B300:2"]} # auto: FP8 if it fits, else NVFP4 | |
| GPU_MEM_GB = {"B300": 288, "B200": 180, "H200": 141, "H100": 80} | |
| GPU_RATES = {"B300": 7.10, "B200": 6.25, "H200": 4.54, "H100": 3.95} # $/GPU-hour on Modal | |
| W = "/weights" | |
| D = "/data" | |
| dl_image = modal.Image.debian_slim(python_version="3.12").pip_install("huggingface_hub[hf_xet]>=0.34") | |
| sglang_image = ( | |
| modal.Image.from_registry("lmsysorg/sglang:beam") | |
| .pip_install("httpx") | |
| .env({"HF_HUB_OFFLINE": "1", "SGLANG_DISABLE_REQUEST_LOGGING": "1"}) | |
| ) | |
| def download(which: str = "nvfp4"): | |
| from huggingface_hub import snapshot_download | |
| repos = {"nvfp4": [(BEAM_REPO, "beam-nvfp4"), (DRAFT_REPO, "dflash2")], "fp8": [(FP8_REPO, "beam-fp8")]}[which] | |
| for repo, sub in repos: | |
| t0 = time.time() | |
| path = snapshot_download(repo, local_dir=f"{W}/{sub}", max_workers=16) | |
| size = sum(f.stat().st_size for f in Path(path).rglob("*") if f.is_file()) | |
| print(f"{repo}: {size / 1e9:.1f} GB in {time.time() - t0:.0f}s", flush=True) | |
| weights.commit() | |
| def start_server(tp, spec, context_len, mem_frac, model_dir="beam-nvfp4", attn="trtllm_mha"): | |
| cmd = [ | |
| "python", "-m", "sglang.launch_server", | |
| "--model-path", f"{W}/{model_dir}", "--served-model-name", "beam", | |
| "--tp", str(tp), "--attention-backend", attn, "--page-size", "64", | |
| "--context-length", str(context_len), "--mem-fraction-static", str(mem_frac), | |
| "--reasoning-parser", "beam", "--tool-call-parser", "beam", | |
| "--host", "127.0.0.1", "--port", "30000", | |
| ] | |
| if spec: | |
| cmd += ["--speculative-algorithm", "DFLASH", "--speculative-draft-model-path", f"{W}/dflash2", | |
| "--speculative-dflash-block-size", "8"] | |
| print("launch:", " ".join(cmd), flush=True) | |
| log = open("/tmp/sglang.log", "w") | |
| return subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT) | |
| def wait_ready(proc, timeout_s): | |
| import httpx | |
| t0 = time.time() | |
| while time.time() - t0 < timeout_s: | |
| if proc.poll() is not None: | |
| raise RuntimeError("sglang exited:\n" + Path("/tmp/sglang.log").read_text()[-6000:]) | |
| try: | |
| if httpx.get("http://127.0.0.1:30000/health", timeout=5).status_code == 200: | |
| return time.time() - t0 | |
| except httpx.HTTPError: | |
| pass | |
| time.sleep(10) | |
| raise TimeoutError("sglang not ready:\n" + Path("/tmp/sglang.log").read_text()[-6000:]) | |
| async def run_phase(items, concurrency, seconds, out_f, label, max_tokens): | |
| """Pull prompts from items with `concurrency` workers; stop issuing after `seconds`.""" | |
| import asyncio | |
| import httpx | |
| queue = asyncio.Queue() | |
| for it in items: | |
| queue.put_nowait(it) | |
| stats = {"n": 0, "tokens": 0, "errors": 0} | |
| t_start = time.time() | |
| deadline = t_start + seconds if seconds else None | |
| async def worker(client): | |
| while not queue.empty(): | |
| if deadline and time.time() > deadline: | |
| return | |
| it = queue.get_nowait() | |
| body = { | |
| "model": "beam", | |
| "messages": [{"role": "user", "content": it["prompt"]}], | |
| "max_tokens": max_tokens, | |
| "temperature": 0.7, | |
| "top_p": 0.99, | |
| "chat_template_kwargs": {"reasoning_effort": "medium"}, | |
| "separate_reasoning": True, | |
| } | |
| t0 = time.time() | |
| try: | |
| r = await client.post("http://127.0.0.1:30000/v1/chat/completions", json=body) | |
| r.raise_for_status() | |
| resp = r.json() | |
| except Exception as e: # keep the phase going; record the failure | |
| stats["errors"] += 1 | |
| out_f.write(json.dumps({"id": it["id"], "phase": label, "error": repr(e)[:300]}) + "\n") | |
| continue | |
| choice = resp["choices"][0] | |
| usage = resp.get("usage") or {} | |
| stats["n"] += 1 | |
| stats["tokens"] += usage.get("completion_tokens", 0) | |
| rec = {k: it.get(k) for k in ("id", "domain", "source", "prompt", "difficulty", "topic", "answer_type")} | |
| rec.update({ | |
| "effort": "medium", | |
| "gold": it["answer"], | |
| "teacher": "beam-nvfp4-selfhost-sglang", | |
| "phase": label, | |
| "reasoning": choice["message"].get("reasoning_content") or "", | |
| "content": choice["message"].get("content") or "", | |
| "finish_reason": choice.get("finish_reason"), | |
| "usage": usage, | |
| "latency_s": round(time.time() - t0, 2), | |
| "time": time.strftime("%Y-%m-%dT%H:%M:%S"), | |
| }) | |
| out_f.write(json.dumps(rec, ensure_ascii=False) + "\n") | |
| out_f.flush() | |
| limits = httpx.Limits(max_connections=concurrency + 8) | |
| async with httpx.AsyncClient(timeout=httpx.Timeout(1800), limits=limits) as client: | |
| await asyncio.gather(*(worker(client) for _ in range(concurrency))) | |
| wall = time.time() - t_start | |
| stats.update({"label": label, "concurrency": concurrency, "wall_s": round(wall, 1), | |
| "out_tok_per_s": round(stats["tokens"] / wall, 1)}) | |
| print("PHASE", json.dumps(stats), flush=True) | |
| return stats | |
| def bench(items: list, parity_ids: list, sweep: list, phase_seconds: int = 300, spec: bool = False, | |
| max_tokens: int = 16384, tag: str = "bench"): | |
| import asyncio | |
| t_boot = time.time() | |
| proc = start_server(tp=2, spec=spec, context_len=32768, mem_frac=0.88) | |
| ready_s = wait_ready(proc, timeout_s=30 * 60) | |
| print(f"server ready after {ready_s:.0f}s", flush=True) | |
| print(subprocess.run(["nvidia-smi", "--query-gpu=name,memory.used,memory.total", "--format=csv"], | |
| capture_output=True, text=True).stdout, flush=True) | |
| out_dir = Path(D) / "selfhost" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| out_path = out_dir / f"{tag}-{time.strftime('%Y%m%d-%H%M%S')}.jsonl" | |
| parity = [it for it in items if it["id"] in set(parity_ids)] | |
| fresh = [it for it in items if it["id"] not in set(parity_ids)] | |
| results = {"ready_s": round(ready_s, 1), "spec": spec, "phases": [], "out_file": str(out_path)} | |
| with open(out_path, "a") as f: | |
| if parity: | |
| results["phases"].append(asyncio.run(run_phase(parity, len(parity), 0, f, "parity", max_tokens))) | |
| data.commit() | |
| pos = 0 | |
| for conc in sweep: | |
| chunk = fresh[pos:] | |
| st = asyncio.run(run_phase(chunk, conc, phase_seconds, f, f"c{conc}", max_tokens)) | |
| pos += st["n"] + st["errors"] | |
| results["phases"].append(st) | |
| data.commit() | |
| proc.terminate() | |
| results["total_s"] = round(time.time() - t_boot, 1) | |
| print("RESULTS", json.dumps(results), flush=True) | |
| return results | |
| def main(ids: str = "data/selfhost_test_ids.txt", parity: str = "data/pilot_ids.txt", | |
| sweep: str = "64,128,256", phase_seconds: int = 300, spec: bool = False): | |
| root = Path(__file__).resolve().parent.parent | |
| pool = {} | |
| for name in ("pool.jsonl", "pool_ext.jsonl"): | |
| for line in open(root / "data" / name): | |
| r = json.loads(line) | |
| pool[r["id"]] = r | |
| wanted = [l.strip() for l in open(root / ids) if l.strip()] | |
| parity_ids = [l.strip() for l in open(root / parity) if l.strip()] | |
| items = [pool[i] for i in parity_ids + wanted] | |
| res = bench.remote(items, parity_ids, [int(x) for x in sweep.split(",")], phase_seconds, spec) | |
| print(json.dumps(res, indent=1)) | |
| TEACHER = "beam-selfhost-sglang" | |
| async def produce(items, n, effort, concurrency, deadline, out_path, max_tokens, log_every=120): | |
| """Long-running generation: n samples per prompt, stop issuing at `deadline`, commit periodically.""" | |
| import asyncio | |
| import httpx | |
| queue = asyncio.Queue() | |
| for it in items: | |
| queue.put_nowait(it) | |
| st = {"prompts": 0, "samples": 0, "tokens": 0, "errors": 0} | |
| t0 = time.time() | |
| out_f = open(out_path, "a") | |
| async def worker(client): | |
| while not queue.empty() and time.time() < deadline: | |
| it = queue.get_nowait() | |
| eff = it.get("_effort", effort) # multi-effort runs tag each queue item with its effort | |
| body = {"model": "beam", "messages": [{"role": "user", "content": it["prompt"]}], | |
| "max_tokens": max_tokens, "temperature": 0.7, "top_p": 0.99, "n": n, | |
| "chat_template_kwargs": {"reasoning_effort": eff}, "separate_reasoning": True} | |
| t1 = time.time() | |
| try: | |
| r = await client.post("http://127.0.0.1:30000/v1/chat/completions", json=body) | |
| r.raise_for_status() | |
| resp = r.json() | |
| except Exception as e: | |
| st["errors"] += 1 | |
| out_f.write(json.dumps({"id": it["id"], "effort": eff, "error": repr(e)[:300]}) + "\n") | |
| continue | |
| usage = resp.get("usage") or {} | |
| per = usage.get("completion_tokens", 0) / max(1, len(resp["choices"])) | |
| for k, choice in enumerate(resp["choices"]): | |
| rec = {k2: it.get(k2) for k2 in ("id", "domain", "source", "prompt", "difficulty", "topic", "answer_type")} | |
| rec.update({"effort": eff, "gold": it["answer"], "teacher": TEACHER, | |
| "sample_idx": k, "reasoning": choice["message"].get("reasoning_content") or "", | |
| "content": choice["message"].get("content") or "", | |
| "finish_reason": choice.get("finish_reason"), | |
| # SGLang reports usage for the whole request; split evenly as an estimate | |
| "usage": {"completion_tokens": round(per), "request_completion_tokens": usage.get("completion_tokens")}, | |
| "latency_s": round(time.time() - t1, 2), "time": time.strftime("%Y-%m-%dT%H:%M:%S")}) | |
| out_f.write(json.dumps(rec, ensure_ascii=False) + "\n") | |
| st["samples"] += 1 | |
| st["prompts"] += 1 | |
| st["tokens"] += usage.get("completion_tokens", 0) | |
| async def reporter(): | |
| while True: | |
| await asyncio.sleep(log_every) | |
| out_f.flush() | |
| await asyncio.to_thread(data.commit) | |
| el = time.time() - t0 | |
| print(f"PROGRESS {time.strftime('%H:%M:%S')} prompts={st['prompts']} samples={st['samples']} " | |
| f"err={st['errors']} tok/s={st['tokens'] / el:.0f} left={queue.qsize()} " | |
| f"stop_in={(deadline - time.time()) / 60:.0f}min", flush=True) | |
| limits = httpx.Limits(max_connections=concurrency + 8) | |
| async with httpx.AsyncClient(timeout=httpx.Timeout(3600), limits=limits) as client: | |
| rep = asyncio.create_task(reporter()) | |
| await asyncio.gather(*(worker(client) for _ in range(concurrency))) | |
| rep.cancel() | |
| out_f.close() | |
| data.commit() | |
| st["wall_s"] = round(time.time() - t0) | |
| return st | |
| def _run_impl(prompts_file: str, tag: str, n: int = 2, effort: str = "medium", concurrency: int = 320, | |
| max_hours: float = 9.0, max_tokens: int = 16384, model: str = "nvfp4", max_dollars: float = 0): | |
| global TEACHER | |
| """Production run over a prompt file on the data Volume. Resumable: (prompt, effort) pairs already in | |
| selfhost/run-<tag>-*.jsonl are skipped (survives preemption). `effort` may list several levels | |
| ("low,high"): every prompt is queued once per level, interleaved so all levels progress together.""" | |
| import asyncio | |
| t_start = time.time() | |
| deadline = t_start + max_hours * 3600 | |
| items = [json.loads(l) for l in open(f"{D}/{prompts_file}")] | |
| out_dir = Path(D) / "selfhost" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| done = set() | |
| for f in out_dir.glob(f"run-{tag}-*.jsonl"): | |
| for line in open(f): | |
| try: | |
| r = json.loads(line) | |
| except json.JSONDecodeError: # torn last line after preemption | |
| continue | |
| if "error" not in r: | |
| done.add((r["id"], r.get("effort", "medium"))) | |
| efforts = effort.split(",") | |
| todo = [dict(it, _effort=e) for it in items for e in efforts if (it["id"], e) not in done] | |
| TEACHER = f"beam-{model}-selfhost-sglang" | |
| print(f"{len(items)} prompts, {len(done)} already done, {len(todo)} to go; n={n} effort={effort}", flush=True) | |
| gpus = subprocess.run(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], | |
| capture_output=True, text=True).stdout.split("\n") | |
| gpus = [g for g in gpus if g.strip()] | |
| kind = next(k for k in GPU_RATES if k in gpus[0]) | |
| tp = len(gpus) | |
| if model == "auto": # FP8 (503 GB) needs ~1 TB of GPU memory; otherwise use NVFP4 (294 GB) | |
| model = "fp8" if tp * GPU_MEM_GB[kind] >= 1000 else "nvfp4" | |
| TEACHER = f"beam-{model}-selfhost-sglang" | |
| model_dir, _ = MODELS[model] | |
| rate = tp * GPU_RATES[kind] + 2.8 # $/h incl. CPU and memory | |
| if max_dollars: | |
| deadline = min(deadline, t_start + max_dollars / rate * 3600) | |
| attn = "fa3" if kind in ("H200", "H100") else "trtllm_mha" | |
| print(f"got {tp}x {kind} (~${rate:.1f}/h); model={model}; attention={attn}; stop issuing in " | |
| f"{(deadline - time.time()) / 3600:.2f}h", flush=True) | |
| proc = start_server(tp=tp, spec=False, context_len=32768, mem_frac=0.88, model_dir=model_dir, attn=attn) | |
| print(f"server ready after {wait_ready(proc, timeout_s=40 * 60):.0f}s", flush=True) | |
| out_path = out_dir / f"run-{tag}-{time.strftime('%Y%m%d-%H%M%S')}.jsonl" | |
| st = asyncio.run(produce(todo, n, effort, concurrency, deadline - 300, out_path, max_tokens)) | |
| proc.terminate() | |
| st["total_h"] = round((time.time() - t_start) / 3600, 2) | |
| print("RUN DONE", json.dumps(st), flush=True) | |
| return st | |
| RUN_KW = dict(image=sglang_image, volumes={W: weights, D: data}, timeout=12 * 3600, memory=262144, cpu=16) | |
| def run(*args, **kwargs): | |
| return _run_impl(*args, **kwargs) | |
| def run_fp8(*args, **kwargs): | |
| return _run_impl(*args, **kwargs) | |
| def run_auto(*args, **kwargs): | |
| return _run_impl(*args, **kwargs) | |
| def launch(prompts: str, tag: str, model: str = "fp8", n: int = 1, effort: str = "medium", | |
| concurrency: int = 384, max_hours: float = 8.0, max_dollars: float = 0, gpu: str = ""): | |
| """prompts: path inside the beam-dory-data Volume (upload first with `modal volume put`). | |
| Takes the first available GPU config from GPU_CHOICES[model]; max_dollars caps the run.""" | |
| fn = {"fp8": run_fp8, "auto": run_auto}.get(model, run) | |
| if gpu: # pin one exact GPU config instead of the fallback list | |
| fn = fn.with_options(gpu=gpu) | |
| res = fn.remote(prompts, tag, n, effort, concurrency, max_hours, 16384, model, max_dollars) | |
| print(json.dumps(res, indent=1)) | |