|
| 1 | +--- |
| 2 | +name: create-remote-bench |
| 3 | +description: Create a new *-remote-bench.sh recipe and run a full CCU-ladder sweep (not just a one-off smoke test) against an already-running, externally-managed inference endpoint (BYO endpoint) instead of launching a server on the GPU runner. Use when bringing up remote-bench for a new model/precision/framework combo, or when asked to benchmark an existing deployment (e.g. a k8s/ArgoCD-managed SGLang/vLLM service) rather than a fresh InferenceX-launched server. Background and rationale: issue #26/#28, PR #27/#30/#31. |
| 4 | +--- |
| 5 | + |
| 6 | +# Create a remote-bench recipe |
| 7 | + |
| 8 | +Remote-bench benchmarks a target InferenceX does not control the lifecycle of — no local |
| 9 | +process, no docker, no GPU on the runner itself. Everything InferenceX normally derives by |
| 10 | +launching the server (image, topology, context length) has to be **self-reported** by |
| 11 | +whoever owns the endpoint instead. |
| 12 | + |
| 13 | +## 1. Find out what's actually behind the endpoint |
| 14 | + |
| 15 | +You need this even though you're not going to touch it — it feeds both the pre-flight |
| 16 | +checks and the ingested artifact's identity fields. |
| 17 | + |
| 18 | +- If the operator exposes a `/discover`-style endpoint (VNG's `inference-cicd` does), curl |
| 19 | + it — it typically returns `base_url`, `gpu_metrics_url`, `chart`, `framework`, `image`, |
| 20 | + `model`, `precision`, `servedName`, `tp` per stack. |
| 21 | +- Otherwise ask the operator directly, or if you have cluster access, |
| 22 | + `kubectl get deployment <name> -n <namespace> -o yaml` and read the container `args`/`image`. |
| 23 | +- **Context length is the one that matters most and is easy to miss**: read the deployed |
| 24 | + `--context-length` (SGLang) / `--max-model-len` (vLLM) argument directly. Do not guess or |
| 25 | + infer it from the model card — get the actual number the server was launched with. |
| 26 | + |
| 27 | +## 2. Required parameters (the recipe's env-var contract) |
| 28 | + |
| 29 | +Every `*-remote-bench.sh` recipe requires, self-reported by the endpoint's operator: |
| 30 | + |
| 31 | +| Var | What it is | Why required | |
| 32 | +|---|---|---| |
| 33 | +| `REMOTE_BASE_URL` | e.g. `http://host/sglang-vanilla` | the actual target to hit | |
| 34 | +| `REMOTE_GPU_TELEMETRY_URL` | DCGM `/metrics`-style endpoint | GPU telemetry is required for remote-bench, not optional (unlike aiperf's own soft-fail default for general use) | |
| 35 | +| `REMOTE_ENGINE_METRICS_URL` | engine's own `/metrics` (e.g. SGLang's) | same — required, not optional | |
| 36 | +| `REMOTE_RUNNER_TYPE` | real, `GPU_KEYS`-resolvable hw string, e.g. `h200-nv` | becomes `RUNNER_TYPE`/`hw` in the ingested artifact; the GH Actions runner label (`cluster:remote-bench`) is **not** a real hardware key and would break `hwToGpuKey()` in InferenceX-app's ingest if used directly | |
| 37 | +| `REMOTE_MAX_CONTEXT_LENGTH` | a *safe* trace-length cap, not necessarily the model's full deployed context window | **confirmed by incident**: without this, aiperf replays trace turns longer than the model supports, relying on server-side auto-truncate — this triggered a silent 100%-GPU hang in SGLang's chunked-prefill continuation on oversized inputs. But setting it to the real deployed context window (e.g. `131072`) is **not automatically safe either** — on a single small/dev GPU (confirmed on an RTX 5090), individual traces near that limit (~120K tokens) still hung in decode after prefill completed cleanly (throughput collapsing to ~0.07 tok/s, never recovering). Also note the available public trace corpora only come in two sizes (unfiltered / `_256k`-capped, see `resolve_trace_source()` in `benchmark_lib.sh`) — there's no small-context variant, so capping below the corpus's shortest trace length (e.g. `32768`) fails outright with `DatasetLoaderError: All N traces exceed --max-context-length`. If a run hangs at the real context window, binary-search downward (e.g. try half the window) to find a cap this specific box's decode can actually sustain, rather than assuming the nominal window is safe. | |
| 38 | + |
| 39 | +Optional: |
| 40 | + |
| 41 | +| Var | What it is | |
| 42 | +|---|---| |
| 43 | +| `REMOTE_RESET_URL` | endpoint to reset KV/prefix cache + router affinity before each concurrency point — a remote target is one long-lived engine across the whole sweep, unlike local recipes which get a fresh process per `conc` job | |
| 44 | + |
| 45 | +On the `remote-bench-e2e.yml` workflow_dispatch side, also required per config (these exist for every |
| 46 | +recipe, but for remote-bench they're pure self-reported metadata rather than values that |
| 47 | +configure anything InferenceX launches): |
| 48 | + |
| 49 | +- `image` — the container image **actually deployed** behind the endpoint (from step 1). |
| 50 | + This is recorded verbatim into the ingested artifact's `image` field — never leave it as |
| 51 | + a placeholder. |
| 52 | +- `model`, `model-prefix`, `framework`, `precision` — identity fields for ingest/labeling. |
| 53 | +- `tp`, `ep`, `dp-attn` (default `tp=1`, `ep=1`, `dp-attn=false`) — topology metadata. Not |
| 54 | + enforced against the real deployment (InferenceX can't verify a black-box endpoint's |
| 55 | + actual topology), so report the real values or the per-GPU throughput math in the |
| 56 | + ingested artifact will be wrong. |
| 57 | + |
| 58 | +## 3. Write the recipe file |
| 59 | + |
| 60 | +One new file: `benchmarks/single_node/agentic/<model_prefix>_<precision>_<framework>-remote-bench.sh`. |
| 61 | + |
| 62 | +Copy an existing one (`glm5.2_fp4_sglang-remote-bench.sh` or `dsv2lite_fp8_sglang-remote-bench.sh`) |
| 63 | +and rename — **the body is model-agnostic and framework-agnostic by design**. Unlike local |
| 64 | +recipes (one file per hardware target, because local server launch args are hw-specific), |
| 65 | +remote-bench never launches a server, so there is no hw-specific tuning to encode. One file |
| 66 | +per model+precision+framework combo is enough; do not create a new file per hardware/cluster. |
| 67 | + |
| 68 | +The launcher naming formula (`runners/launch_bench-client.sh`) is: |
| 69 | +``` |
| 70 | +benchmarks/single_node/agentic/${EXP_NAME%%_*}_${PRECISION}_${FRAMEWORK}-remote-bench.sh |
| 71 | +``` |
| 72 | +So `exp-name`'s first underscore-delimited segment must equal `model-prefix`, and the |
| 73 | +filename must match `<model-prefix>_<precision>_<framework>-remote-bench.sh` exactly. |
| 74 | + |
| 75 | +Do not edit `benchmark_lib.sh` or any existing recipe for a new model — this workflow is |
| 76 | +purely additive. |
| 77 | + |
| 78 | +## 4. Runner |
| 79 | + |
| 80 | +Remote-bench dispatches to the `cluster:remote-bench` label |
| 81 | +(`configs/runners.yaml`), currently backed by one real non-GPU controller box |
| 82 | +(`bench-client_01`). Reuse it — you don't need a new runner per model/target, since the |
| 83 | +controller only drives aiperf over the network; it never touches the GPU itself. Only |
| 84 | +register a new runner if the existing controller is saturated or unreachable from a new |
| 85 | +target's network. |
| 86 | + |
| 87 | +## 5. Check the KV pool before picking a CCU ladder |
| 88 | + |
| 89 | +Don't guess concurrency values for a sweep — find out how much KV cache capacity the |
| 90 | +endpoint's engine actually has first. Too low and you leave throughput on the table; too |
| 91 | +high and you push the engine into the queuing/decode-hang territory described in step 2's |
| 92 | +`REMOTE_MAX_CONTEXT_LENGTH` entry. |
| 93 | + |
| 94 | +For SGLang, the live `/metrics` endpoint (your `REMOTE_ENGINE_METRICS_URL`) exposes the KV |
| 95 | +pool's total token capacity as a gauge set once at server startup — you can read it before |
| 96 | +ever sending a request: |
| 97 | + |
| 98 | +```bash |
| 99 | +curl -s <REMOTE_ENGINE_METRICS_URL> | grep sglang:max_total_num_tokens |
| 100 | +``` |
| 101 | + |
| 102 | +(This is the same number that later shows up in the aggregated result's |
| 103 | +`server_metrics.kv_cache.gpu_total_tokens` — pulling it up front means you don't have to |
| 104 | +run a throwaway job just to see it.) |
| 105 | + |
| 106 | +Use it to size the ladder: |
| 107 | + |
| 108 | +``` |
| 109 | +max_conc ≈ (kv_pool_tokens × target_utilization) / REMOTE_MAX_CONTEXT_LENGTH |
| 110 | +``` |
| 111 | + |
| 112 | +`target_utilization` around 0.6-0.8 — pushing toward 1.0 is exactly the KV-pressure regime |
| 113 | +that causes decode to collapse (see step 2). Build a doubling ladder up to that estimate — |
| 114 | +`1, 2, 4, 8, ..., max_conc` — dropping the last step if it overshoots. Don't reuse a ladder |
| 115 | +computed for a different model/hardware/`REMOTE_MAX_CONTEXT_LENGTH` combo; the math above |
| 116 | +depends on all three. |
| 117 | + |
| 118 | +vLLM targets don't expose an equivalent live token-count gauge the same way |
| 119 | +(`vllm:kv_cache_usage_perc` is a percentage, not a token count; the raw number is normally |
| 120 | +parsed from the server's own startup log, which remote-bench never captures — see step 8). |
| 121 | +Ask the endpoint's operator directly for KV cache token capacity, or derive it from |
| 122 | +`--gpu-memory-utilization` and the model's per-token KV footprint. |
| 123 | + |
| 124 | +## 6. Dispatch |
| 125 | + |
| 126 | +There's one workflow: `remote-bench-e2e.yml`. It takes a JSON array of configs and |
| 127 | +matrix-fans each entry to its own job, then aggregates via `collect-results` + |
| 128 | +`calc-success-rate` into one combined summary. A single-config smoke test is just a |
| 129 | +one-element array — there's no separate single-dispatch workflow anymore (an earlier |
| 130 | +`remote-bench.yml` existed for that and was removed once `remote-bench-e2e.yml` covered |
| 131 | +both cases; don't recreate it). |
| 132 | + |
| 133 | +Smoke test (one config): |
| 134 | + |
| 135 | +```bash |
| 136 | +gh workflow run remote-bench-e2e.yml -R vngcloud/InferenceX --ref <branch> \ |
| 137 | + -f configs='[ |
| 138 | + {"exp-name": "<model_prefix>_smoke", "conc": "1", "duration": "<seconds>", |
| 139 | + "image": "<real deployed image>", "model": "<HF repo id>", |
| 140 | + "model-prefix": "<model_prefix>", "framework": "sglang", "precision": "<precision>", |
| 141 | + "remote-base-url": "<url>", "remote-gpu-telemetry-url": "<url>", |
| 142 | + "remote-engine-metrics-url": "<url>", "remote-runner-type": "<hw string>", |
| 143 | + "remote-max-context-length": "<real context length>"} |
| 144 | + ]' |
| 145 | +``` |
| 146 | + |
| 147 | +Full sweep (the CCU ladder) — same shape, one object per conc value: |
| 148 | + |
| 149 | +```bash |
| 150 | +gh workflow run remote-bench-e2e.yml -R vngcloud/InferenceX --ref <branch> \ |
| 151 | + -f configs='[ |
| 152 | + {"exp-name": "<model_prefix>_c1", "conc": "1", "duration": "<seconds>", ...}, |
| 153 | + {"exp-name": "<model_prefix>_c2", "conc": "2", "duration": "<seconds>", ...}, |
| 154 | + {"exp-name": "<model_prefix>_c4", "conc": "4", "duration": "<seconds>", ...} |
| 155 | + ]' |
| 156 | +``` |
| 157 | + |
| 158 | +Every object needs the full field set shown in the smoke-test example above — only `conc` |
| 159 | +(and `exp-name`, so results stay distinguishable) should vary across the ladder. |
| 160 | + |
| 161 | +**No `REMOTE_RESET_URL` configured means the ladder isn't a clean comparison.** A remote |
| 162 | +target is one persistent engine across the whole sweep (unlike local recipes, which get a |
| 163 | +fresh process per `conc` job) — without a reset endpoint, each subsequent point in the |
| 164 | +ladder inherits KV/prefix cache warmth from every prior point (and from any earlier runs |
| 165 | +against the same endpoint). Confirmed in practice: repeated runs against the same dataset |
| 166 | +with no reset showed ~92-94% prefix-cache hit rate regardless of concurrency. If the |
| 167 | +endpoint's operator can't provide a real reset endpoint, treat ladder results as smoke-test |
| 168 | +validation that the sweep mechanics work, not as clean per-concurrency performance numbers. |
| 169 | + |
| 170 | +`workflow_dispatch` only works once the workflow file exists on the **default branch** |
| 171 | +(`main`) — you cannot dispatch a brand-new `remote-bench-e2e.yml`-style workflow from a |
| 172 | +feature branch before it merges. For pre-merge validation, use the debug loop below instead. |
| 173 | + |
| 174 | +## 7. Debug loop (do this before wiring into CI) |
| 175 | + |
| 176 | +Mirrors `/debug-runs`'s tight-loop philosophy: reproduce directly rather than iterating |
| 177 | +through full CI dispatch cycles you can't even trigger yet pre-merge. |
| 178 | + |
| 179 | +1. SSH onto the controller box (`bench-client_01` or whichever `cluster:remote-bench` |
| 180 | + runner), clone/checkout the branch under test. |
| 181 | +2. Run `runners/launch_bench-client.sh` directly, exporting every env var |
| 182 | + `benchmark-tmpl.yml` would normally set (`EXP_NAME`, `MODEL`, `MODEL_PREFIX`, `FRAMEWORK`, |
| 183 | + `PRECISION`, `CONC`, `DURATION`, `SCENARIO_TYPE=agentic-coding`, |
| 184 | + `SCENARIO_SUBDIR=agentic/`, `IS_AGENTIC=1`, `KV_OFFLOADING=none`, the `REMOTE_*` vars, |
| 185 | + and `GITHUB_WORKSPACE`/`RUNNER_NAME` pointing at your checkout) — plus |
| 186 | + `RESULT_DIR`/`INFMAX_CONTAINER_WORKSPACE` overridden to a real path on the box (the |
| 187 | + launcher already does this; every other launcher assumes a docker bind mount that |
| 188 | + doesn't exist here). |
| 189 | +3. Before touching aiperf, curl the three required URLs yourself — `/health`, |
| 190 | + `REMOTE_GPU_TELEMETRY_URL`, `REMOTE_ENGINE_METRICS_URL` — with both `-I` (HEAD) and a |
| 191 | + plain GET. A proxy/exporter that answers GET but 501s on HEAD is a real thing you may |
| 192 | + hit; if aiperf's own reachability probe (HEAD-first, GET-fallback) still reports an |
| 193 | + endpoint unreachable despite curl succeeding, don't assume it's fixed — retest after any |
| 194 | + endpoint-side change, this has been flaky/order-dependent in practice. |
| 195 | +4. If the server hangs mid-run (no crash, no new log lines, but GPU utilization pegged at |
| 196 | + ~100%), check `REMOTE_MAX_CONTEXT_LENGTH` against the traces actually being replayed |
| 197 | + first — this exact symptom was chunked-prefill continuation on an oversized, |
| 198 | + auto-truncated input. `kubectl logs <pod> -n <namespace>` on the endpoint's actual pod is |
| 199 | + the only place server-side errors (e.g. `Health check failed. Server couldn't get a |
| 200 | + response from detokenizer...`) show up; nothing about them reaches the aiperf client or |
| 201 | + GH Actions logs. |
| 202 | +5. Iterate on the node until a run completes with `replay_rc=0` and real |
| 203 | + `profile_export_aiperf.{csv,json}` / `server_metrics_export.json` files with actual data |
| 204 | + in them (not empty) before considering the recipe done. |
| 205 | + |
| 206 | +## 8. What a real, ingest-able run produces |
| 207 | + |
| 208 | +For `scenario-type: agentic-coding` (which all remote-bench recipes are), `benchmark-tmpl.yml` |
| 209 | +uploads: |
| 210 | + |
| 211 | +- `bmk_agentic_<name>` — the aggregated `agg_*.json` result (throughput, latency, the |
| 212 | + identity fields from step 2 above). This is what ultimately reaches InferenceX-app's |
| 213 | + ingest. |
| 214 | +- `agentic_<name>` — `results/**` (aiperf's raw artifacts: `profile_export_aiperf.{csv,json}`, |
| 215 | + `server_metrics_export.{csv,json}`, timeslices, `aiperf.log`). |
| 216 | + |
| 217 | +`server_logs_<name>` is still uploaded, but for remote-bench it's just the aiperf **client's** |
| 218 | +own `benchmark.log` (aiperf's own startup/runner log), not an actual inference-engine server |
| 219 | +log — there's no locally-launched process to redirect. Don't confuse the two when reading it; |
| 220 | +the real engine-side story only lives in `kubectl logs` on the endpoint's actual pod. |
| 221 | +`gpu_metrics_<name>` (the local `nvidia-smi`/`amd-smi` capture) is **not** produced — no |
| 222 | +local GPU on the controller box. That data still exists, just inside `agentic_<name>` via |
| 223 | +aiperf's own GPU telemetry scrape (`gpu_telemetry_export.jsonl`) instead of a separate local |
| 224 | +capture. |
| 225 | + |
| 226 | +Sanity-check before calling a run "done": open `agg_*.json` and confirm `hw` is a real |
| 227 | +`GPU_KEYS`-resolvable string (not `cluster:remote-bench`), `image` is the real deployed |
| 228 | +image (not a placeholder), and the throughput numbers are non-zero. |
0 commit comments