|
| 1 | +# GPU-rental testing runbook (for agents) |
| 2 | + |
| 3 | +> **Audience: an agent running colocate GPU tests on a rental platform |
| 4 | +> without a human in the loop.** It tells you how to provision, run, |
| 5 | +> monitor, and — critically — tear down a rented GPU pod safely and |
| 6 | +> cheaply. Everything here was learned the hard way across the RunPod |
| 7 | +> sessions in [`implementation_log.md`](implementation_log.md). |
| 8 | +> |
| 9 | +> Companion docs: [`cheap_host_test_plan.md`](cheap_host_test_plan.md) |
| 10 | +> (cost-tier matrix, test plan) and [`sglang_patch.md`](sglang_patch.md) |
| 11 | +> (the sglang patch the tests exercise). |
| 12 | +
|
| 13 | +## When you need this |
| 14 | + |
| 15 | +The colocate tests (`tests/colocate/test_*`, phases 4/6/7) need **NVIDIA |
| 16 | +MPS**, which needs a container started with `--ipc=host`. Use this |
| 17 | +runbook whenever a task asks you to GPU-validate colocate. |
| 18 | + |
| 19 | +**Modal does not work for colocate.** Modal sandboxes run under gVisor, |
| 20 | +whose nvproxy does not implement MPS multiplexing — the MPS-required |
| 21 | +tests `pytest.skip` there, they do not run. Use a real `--ipc=host` |
| 22 | +host: **RunPod** (default here), Vast.ai, Lambda, or bare metal. |
| 23 | + |
| 24 | +## Hard rules — follow these every time |
| 25 | + |
| 26 | +You are spending real money and sharing an account with other agents. |
| 27 | + |
| 28 | +1. **Check for other pods before you provision.** |
| 29 | + `runpodctl pod list -o json`. If a pod you did **not** create is |
| 30 | + running, never `stop`/`delete` it. (The deprecated `runpodctl get |
| 31 | + pod` can print an *empty* list while pods exist — always use |
| 32 | + `pod list -o json`.) |
| 33 | +2. **Always pass `--terminate-after`** (≈3 h out) when creating a pod. |
| 34 | + It is a backstop: if you lose track, the pod self-destructs instead |
| 35 | + of billing forever. |
| 36 | +3. **Always tear the pod down** as soon as the run finishes — pass or |
| 37 | + fail. Then verify: `runpodctl pod get <id>` must say `pod not found`. |
| 38 | +4. **Watch the balance.** `runpodctl user`. A 4×H100 is ~$13/hr. Do not |
| 39 | + start a run that would drain the balance toward $0 — that stops |
| 40 | + *every* pod on the account, including other agents'. |
| 41 | +5. **One run, then capture and tear down.** Do not open-endedly iterate |
| 42 | + on a billing pod. If a real (non-environment) failure needs code |
| 43 | + changes, tear down first, fix locally, re-provision. |
| 44 | +6. **Surface, don't silently proceed,** if you find another agent's pod |
| 45 | + that your run would starve, or if the balance is too low for one run. |
| 46 | + |
| 47 | +## Prerequisites (already set up on this machine) |
| 48 | + |
| 49 | +- `runpodctl` installed and authenticated — API key in |
| 50 | + `~/.runpod/config.toml`, SSH key at `~/.runpod/ssh/runpodctl-ssh-key` |
| 51 | + (registered on the account). Check: `runpodctl user` prints a balance. |
| 52 | +- An **`HF_TOKEN`** is required for the Qwen3-8B tests (unauthenticated |
| 53 | + HF Hub requests get rate-limited — see failure modes). The tiny |
| 54 | + Qwen3-0.6B tests do not need it. Ask the user for the token if you do |
| 55 | + not have one; never commit it anywhere. |
| 56 | + |
| 57 | +## Workflow (RunPod — the ready path) |
| 58 | + |
| 59 | +RunPod is the platform set up on this machine and used for every GPU |
| 60 | +run to date. Vast.ai is a working alternative — see the section after |
| 61 | +this one. |
| 62 | + |
| 63 | +### 1 — Provision |
| 64 | + |
| 65 | +```bash |
| 66 | +runpodctl pod create --name colocate-<purpose> \ |
| 67 | + --gpu-id "NVIDIA H100 80GB HBM3" --gpu-count <N> \ |
| 68 | + --template-id runpod-torch-v240 \ |
| 69 | + --container-disk-in-gb 200 --ports "22/tcp" \ |
| 70 | + --terminate-after "$(date -u -v+3H +%Y-%m-%dT%H:%M:%SZ)" -o json |
| 71 | +``` |
| 72 | + |
| 73 | +- GPU: `"NVIDIA H100 80GB HBM3"` (H100 SXM). `runpodctl gpu list` for |
| 74 | + others. Only **sm90+** (H100 / H200 / B200) — the bundled `sgl_kernel` |
| 75 | + wheel has no Ampere/Ada kernels. |
| 76 | +- Template `runpod-torch-v240` = `runpod/pytorch:2.4.0-py3.11-cuda12.4.1-devel-ubuntu22.04` |
| 77 | + — the validated image. RunPod "Pods" get `--ipc=host` by default. |
| 78 | +- `--gpu-count`: see the sizing table below. |
| 79 | +- The create call returns the pod `id` — keep it. |
| 80 | + |
| 81 | +### 2 — Wait for SSH (it is slow: 1–8+ min) |
| 82 | + |
| 83 | +The `.ssh.ip` / `.ssh.port` fields appear in `runpodctl pod get <id> |
| 84 | +-o json` **before** SSH actually accepts connections. Poll until a real |
| 85 | +connection succeeds: |
| 86 | + |
| 87 | +```bash |
| 88 | +ssh -i ~/.runpod/ssh/runpodctl-ssh-key -o StrictHostKeyChecking=no \ |
| 89 | + -o UserKnownHostsFile=/dev/null -o ConnectTimeout=15 \ |
| 90 | + -p <port> root@<ip> 'echo ok' |
| 91 | +``` |
| 92 | + |
| 93 | +> **zsh gotcha:** do not put ssh options in a shell variable — zsh does |
| 94 | +> not word-split unquoted variables, so `ssh $OPTS ...` passes them as |
| 95 | +> one bad argument. Inline every option. |
| 96 | +
|
| 97 | +### 3 — Deploy |
| 98 | + |
| 99 | +```bash |
| 100 | +ssh ... 'cd /root && git clone --depth=1 -b feature/colocate-training-inference \ |
| 101 | + https://github.com/zhubohao911/TorchSpec.git' |
| 102 | +``` |
| 103 | + |
| 104 | +If the code/patch you want to test is **committed and pushed**, the |
| 105 | +clone already has it. If it is only local (uncommitted), `scp` the |
| 106 | +files onto the pod after cloning. |
| 107 | + |
| 108 | +### 4 — Run (detached, with an exit-code file) |
| 109 | + |
| 110 | +Write a launcher on the pod and run it with `nohup … & disown` so it |
| 111 | +survives the SSH session closing. Capture the exit code to a file you |
| 112 | +can poll: |
| 113 | + |
| 114 | +```bash |
| 115 | +# /root/launcher.sh on the pod: |
| 116 | +cd /root/TorchSpec |
| 117 | +export HF_TOKEN=<token> # for Qwen3-8B tests |
| 118 | +export SGLANG_PATCH_VERSION=v0.5.10.post1 |
| 119 | +export SGLANG_COMMIT=94f03a39dbd39edfc2b118b5357bbbadaaa9ad28 |
| 120 | +export CUDA_VISIBLE_DEVICES=0,1,2,3 # see note below |
| 121 | +bash scripts/colocate/run_smoke_host.sh [--full | --tests=a.py,b.py] |
| 122 | +echo $? > /root/run.rc |
| 123 | +``` |
| 124 | + |
| 125 | +Launch: `nohup bash /root/launcher.sh > /root/run.log 2>&1 & disown`. |
| 126 | + |
| 127 | +- `run_smoke_host.sh` defaults to `SGLANG_PATCH_VERSION=v0.5.10.post1`; |
| 128 | + it clones sglang, applies the patches, builds, and runs pytest. |
| 129 | +- `--full` runs the whole matrix; `--tests=` runs specific files (use |
| 130 | + this to skip already-passed tests on a re-run). |
| 131 | +- **`CUDA_VISIBLE_DEVICES` note:** `run_smoke_host.sh` only auto-sets |
| 132 | + all 4 GPUs for `--full`. With `--tests=`, pre-export |
| 133 | + `CUDA_VISIBLE_DEVICES=0,1,2,3` yourself or the multi-GPU tests see |
| 134 | + one GPU and skip. |
| 135 | + |
| 136 | +### 5 — Monitor |
| 137 | + |
| 138 | +Poll the **remote** files, not a local background job: |
| 139 | + |
| 140 | +```bash |
| 141 | +ssh ... 'cat /root/run.rc 2>/dev/null || echo RUNNING; tail -8 /root/run.log' |
| 142 | +``` |
| 143 | + |
| 144 | +`run.rc` existing = run finished (`0` = all passed). The colocate |
| 145 | +failure signature is a **hang on the first P2P recv** — if the log |
| 146 | +stops advancing for many minutes mid-step, that is the diagnostic. |
| 147 | + |
| 148 | +### 6 — Tear down (every time) |
| 149 | + |
| 150 | +```bash |
| 151 | +scp ... root@<ip>:/root/TorchSpec/colocate-smoke-report.txt /tmp/ # keep the report |
| 152 | +runpodctl pod stop <id> && runpodctl pod delete <id> |
| 153 | +runpodctl pod get <id> # must say: pod not found |
| 154 | +runpodctl user # confirm currentSpendPerHr dropped |
| 155 | +``` |
| 156 | + |
| 157 | +## Vast.ai (alternative platform) |
| 158 | + |
| 159 | +Vast.ai is a documented alternative — it ran the 4×H100 `--full` suite |
| 160 | +in sessions #4/#5 ([`implementation_log.md`](implementation_log.md)), |
| 161 | +and is often cheaper than RunPod. The `vastai` CLI (v1.0.x) is |
| 162 | +installed, **but not authenticated on this machine.** Before an agent |
| 163 | +can use Vast autonomously, the user must run it once: |
| 164 | + |
| 165 | +```bash |
| 166 | +vastai set api-key <KEY> # key from the vast.ai console |
| 167 | +``` |
| 168 | + |
| 169 | +All the same constraints and **hard rules** above apply (check other |
| 170 | +instances, watch balance, tear down every time). Vast On-Demand |
| 171 | +instances default to `--ipc=host`; choose a "Direct" net-type host with |
| 172 | +a good reliability score and a CUDA 12.x + Python 3.11 PyTorch image. |
| 173 | +sm90+ only, same as RunPod. |
| 174 | + |
| 175 | +The workflow mirrors the RunPod one — only the CLI differs: |
| 176 | + |
| 177 | +| Step | RunPod | Vast.ai | |
| 178 | +|---|---|---| |
| 179 | +| find capacity | `runpodctl gpu list` | `vastai search offers 'gpu_name=H100_SXM num_gpus=4 reliability>0.98'` | |
| 180 | +| provision | `runpodctl pod create …` | `vastai create instance <offer-id> --image <pytorch-cu124-img> --disk 200 --ssh --direct` | |
| 181 | +| list | `runpodctl pod list -o json` | `vastai show instances` | |
| 182 | +| SSH endpoint | `.ssh.ip` / `.ssh.port` | `vastai ssh-url <id>` | |
| 183 | +| **tear down** | `pod stop` + `pod delete` | **`vastai destroy instance <id>`** | |
| 184 | + |
| 185 | +Run `vastai search offers --help` / `vastai create instance --help` for |
| 186 | +exact field syntax — query fields and image flags change between CLI |
| 187 | +versions. |
| 188 | + |
| 189 | +> **Two Vast-specific cautions:** |
| 190 | +> - **`stop instance` is not enough** — a stopped Vast instance still |
| 191 | +> **bills for storage**. Only `destroy instance` (irreversible — |
| 192 | +> deletes the disk) fully stops billing. Always `destroy` when done. |
| 193 | +> - **No `--terminate-after` backstop.** RunPod self-destructs a lost |
| 194 | +> pod; Vast does not. The "always tear down" rule is therefore |
| 195 | +> load-bearing on Vast — never leave an instance unattended. |
| 196 | +
|
| 197 | +## GPU sizing |
| 198 | + |
| 199 | +| Test | GPUs | Model | ~Time (after setup) | |
| 200 | +|---|---|---|---| |
| 201 | +| `test_colocate_tiny.py` | 1 | Qwen3-0.6B | ~4 min | |
| 202 | +| `test_colocate_tp2.py` (`engine_tp_size=2`) | 2 | Qwen3-0.6B | ~2 min | |
| 203 | +| `run_smoke_host.sh --full` (13 tests) | 4 | Qwen3-0.6B + Qwen3-8B | ~22 min | |
| 204 | + |
| 205 | +Setup (pip install + sglang build) adds ~5–12 min on top, once per pod. |
| 206 | + |
| 207 | +## Known failure modes — NOT your patch's bug |
| 208 | + |
| 209 | +| Symptom | Cause | Action | |
| 210 | +|---|---|---| |
| 211 | +| `libnuma.so.1: cannot open shared object file` | RunPod image lacks it | `run_smoke_host.sh` already apt-installs it; if running sglang by hand, `apt-get install -y libnuma1` | |
| 212 | +| HF Hub `429 Too Many Requests` on Qwen3-8B | unauthenticated HF requests rate-limited | set `HF_TOKEN` | |
| 213 | +| pod returns `404 pod not found` / SSH dies mid-run | RunPod infra flakiness (some datacenters worse) | re-provision once; if it repeats, report | |
| 214 | +| SSH never comes up after ~10 min | slow/bad pod | delete it, re-provision | |
| 215 | +| multi-GPU test SKIPs (sees 1 GPU) | `--tests=` didn't set `CUDA_VISIBLE_DEVICES` | pre-export `CUDA_VISIBLE_DEVICES=0,1,2,3` | |
| 216 | +| `Unknown RoPE scaling type default` | old TorchSpec checkout (pre-`be399a0`) | clone current `feature/colocate-training-inference` | |
| 217 | + |
| 218 | +## Cost reference |
| 219 | + |
| 220 | +| Pod | Rate | One run (incl. setup) | |
| 221 | +|---|---|---| |
| 222 | +| 1×H100 SXM | ~$3.3/hr | tiny smoke ≈ $1–2 | |
| 223 | +| 2×H100 SXM | ~$6.6/hr | tp2 ≈ $3–4 | |
| 224 | +| 4×H100 SXM | ~$13/hr | `--full` ≈ $8–12 | |
| 225 | + |
| 226 | +Rates above are RunPod. Vast.ai spot is usually cheaper (~$2/hr for |
| 227 | +1×H100, ~$10–11/hr for 4×H100) but availability and host reliability |
| 228 | +vary more. |
| 229 | + |
| 230 | +Keep the pod alive only for the run. Idle time is pure waste — tear |
| 231 | +down immediately on completion. |
0 commit comments