File size: 7,313 Bytes
9f2303a | 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 | """Rent a dedicated GPU node on Vast.ai and run vLLM (OpenAI-compatible API).
python deploy/vast_node.py offers # list the cheapest available machines
python deploy/vast_node.py up [--offer ID] [--model lightning-nvfp4]
python deploy/vast_node.py status # print base_url, ready to paste into config.json
python deploy/vast_node.py down # destroy the instance and stop billing
Requires VAST_API_KEY. The node's vLLM API key is random and stored in deploy/.node.json (chmod 600, never committed).
"""
import argparse
import json
import os
import secrets
import sys
import time
import urllib.request
from pathlib import Path
API = "https://console.vast.ai/api/v0"
STATE = Path(__file__).parent / ".node.json"
MODELS = {
# Blackwell (RTX PRO 6000) runs NVFP4 natively: ~18 GB of weights, leaving 70 GB+ for the KV cache
"lightning-nvfp4": {"hf": "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4", "served": "kefuro-base",
"extra": ["--max-model-len", "32768", "--gpu-memory-utilization", "0.90"]},
"lightning-bf16": {"hf": "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "served": "kefuro-base",
"extra": ["--max-model-len", "32768", "--gpu-memory-utilization", "0.92"]},
# BF16 base + runtime LoRA loading (scp the adapter after boot, then POST /v1/load_lora_adapter)
"lightning-bf16-lora": {"hf": "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "served": "kefuro-base",
"extra": ["--max-model-len", "16384", "--gpu-memory-utilization", "0.92", "--enable-lora", "--max-lora-rank", "16"]},
# BF16 + tool calling + reasoning parser (per the NVIDIA model card), for tau-bench style agent evals
"lightning-bf16-tools": {"hf": "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16", "served": "kefuro-base",
"extra": ["--max-model-len", "65536", "--gpu-memory-utilization", "0.92", "--enable-prefix-caching",
"--reasoning-parser", "nemotron_v3", "--enable-auto-tool-choice", "--tool-call-parser", "qwen3_coder",
"--enable-lora", "--max-lora-rank", "16"]},
"nano3-fp8": {"hf": "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8", "served": "kefuro-nano3",
"extra": ["--max-model-len", "32768", "--gpu-memory-utilization", "0.90"]},
}
GPU_PREFS = ["RTX PRO 6000 WS", "RTX PRO 6000 S", "H100 SXM", "H100 NVL"]
def _req(method, path, body=None):
key = os.environ["VAST_API_KEY"]
req = urllib.request.Request(API + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
return json.load(r)
def offers(limit=8):
out = []
for g in GPU_PREFS:
q = {"gpu_name": {"eq": g}, "num_gpus": {"eq": 1}, "rentable": {"eq": True}, "verified": {"eq": True},
"reliability2": {"gte": 0.98}, "inet_down": {"gte": 500}, "disk_space": {"gte": 150},
"order": [["dph_total", "asc"]], "type": "on-demand", "limit": 20}
out += _req("POST", "/bundles/", q).get("offers", [])
out.sort(key=lambda o: o["dph_total"])
return out[:limit]
def cmd_offers(_):
for o in offers():
print(f"id={o['id']:<10} {o['gpu_name']:16s} {o['gpu_ram']/1024:.0f}GB ${o['dph_total']:.3f}/h "
f"rel={o.get('reliability2', 0):.3f} down={o.get('inet_down', 0):.0f}Mbps {o.get('geolocation', '')}")
def cmd_up(a):
if STATE.exists():
sys.exit(f"A node record already exists at {STATE}; run down before up")
m = MODELS[a.model]
offer = a.offer or offers(1)[0]["id"]
api_key = "sk-cs-" + secrets.token_hex(24)
args = ["--model", m["hf"], "--served-model-name", m["served"], "--host", "0.0.0.0", "--port", "8000",
"--api-key", api_key, "--trust-remote-code", *m["extra"]]
body = {"client_id": "me", "image": "vllm/vllm-openai:latest", "disk": 150, "runtype": "args",
"args": args,
# without an HF token the Xet download path hangs on some hosts; force plain HTTP downloads
"env": {"-p 8000:8000": "1", "HF_HUB_DISABLE_XET": "1", "HF_HUB_ENABLE_HF_TRANSFER": "0", "VLLM_ALLOW_RUNTIME_LORA_UPDATING": "True"}, "label": "kefuro"}
if a.ssh:
# SSH mode: Vast's init owns the entrypoint, vLLM starts in the background from onstart; SSH in to train
body.update({"runtype": "ssh_direc ssh_proxy", "args": None,
"onstart": "nohup vllm serve " + " ".join(args[1:2]) + " " + " ".join(args[2:]) + " > /root/vllm.log 2>&1 &"})
body.pop("args")
r = _req("PUT", f"/asks/{offer}/", body)
if not r.get("success"):
sys.exit(f"create failed: {r}")
if a.ssh:
pub = Path(os.path.expanduser("~/.ssh/id_ed25519.pub")).read_text().strip()
time.sleep(5)
print("attach ssh key:", _req("POST", f"/instances/{r['new_contract']}/ssh/", {"ssh_key": pub}).get("success"))
STATE.write_text(json.dumps({"instance_id": r["new_contract"], "offer": offer, "model": a.model, "ssh": bool(a.ssh),
"served": m["served"], "api_key": api_key, "created": time.time()}))
STATE.chmod(0o600)
print(f"instance {r['new_contract']} created on offer {offer}; use status to see ports and readiness")
def _instance(st):
return _req("GET", f"/instances/{st['instance_id']}/").get("instances") or {}
def cmd_status(_):
st = json.loads(STATE.read_text())
i = _instance(st)
ports = (i.get("ports") or {}).get("8000/tcp") or []
url = f"http://{i.get('public_ipaddr', '').strip()}:{ports[0]['HostPort']}/v1" if ports else None
hours = (time.time() - st["created"]) / 3600
ssh = None
if st.get("ssh") and i.get("public_ipaddr"):
p22 = ((i.get("ports") or {}).get("22/tcp") or [{}])[0].get("HostPort")
ssh = f"ssh -p {p22} root@{i['public_ipaddr'].strip()}" if p22 else f"ssh -p {i.get('ssh_port')} root@{i.get('ssh_host')}"
print(json.dumps({"instance": st["instance_id"], "state": i.get("actual_status"), "gpu": i.get("gpu_name"), "ssh": ssh,
"dph": i.get("dph_total"), "hours": round(hours, 2), "spent_est": round(hours * (i.get("dph_total") or 0), 2),
"base_url": url, "model": st["served"], "api_key_file": str(STATE)}, indent=2))
def cmd_down(_):
st = json.loads(STATE.read_text())
r = _req("DELETE", f"/instances/{st['instance_id']}/")
print(r)
if r.get("success"):
STATE.unlink()
if __name__ == "__main__":
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("offers")
u = sub.add_parser("up")
u.add_argument("--offer", type=int)
u.add_argument("--model", default="lightning-nvfp4", choices=list(MODELS))
u.add_argument("--ssh", action="store_true", help="enable SSH (vLLM starts in the background from onstart), for training on the same machine")
sub.add_parser("status")
sub.add_parser("down")
a = p.parse_args()
{"offers": cmd_offers, "up": cmd_up, "status": cmd_status, "down": cmd_down}[a.cmd](a)
|