git:20260728.dd3b07b to git:20260802.980063c

207 added, 500 removed. Audit B to A.

---
name: hyperloom.inference_optimizer.multi_node
description: |
Multi-node companion to the inference_optimizer skill. Use when the user
prompt asks for inference optimization that needs more GPU / memory than
a single pod provides (i.e. ``nodes >= 2``) — typical prompt signals are
``Nodes=N`` / ``N pods`` / ``TP=N`` larger than one pod's GPU count, or
- any model that cannot fit on one pod's GPUs. Drives a session-scoped
- SaFE RayJob through the ``hyperloom.inference_optimizer.multi_node`` Python CLI.
+ any model that cannot fit on one pod's GPUs. Drives a platform-provisioned
+ multi-node cluster through the ``hyperloom.inference_optimizer.multi_node``
+ Python CLI.
globs:
- "**/multi_node/**"
- "**/multi-node/**"
---
- # Multi-Node RayJob Skill
-
- Drive every RayJob lifecycle action through the Python CLI below. **Never
- `ray.init`, `kubectl`, or raw `curl` to SaFE / Ray Dashboard.** All state
- is in the state file resolved from `$MULTI_NODE_STATE_FILE` (or
- `$INFERENCE_OPTIMIZER_CURRENT_SESSION_DIR/runtime/multi_node_state.json`
- when the session is pinned; one of the two MUST be set — there is no
- `/tmp` default). It is sandbox-local (lost on sandbox
- recreate — re-running any subcommand reads back from SaFE / Ray
- Dashboard and rewrites the file), maintained by the CLI.
+ # Multi-Node Skill (infera + rayjob backends)
- | Action | Use | Not |
- |-----------------|-------------------------------------------|-----------------------------------------------------------|
- | Create RayJob | `create-rayjob` | `curl POST /workloads`, `kubectl create -f rayjob.yaml` |
- | Check phase | `create-rayjob` (idempotent — resumes) | `curl /workloads/{id}`, `kubectl get rayjob` |
- | Restart server | `restart-server` | `kubectl exec ... sglang.launch_server` |
- | Stop | `stop-multi-job` | `curl POST .../stop` |
+ **The cluster is not created here.** The platform provisions it (an
+ `InferaDeployment` or a `RayJob`) before the optimizer starts and hands it over
+ through the `HYPERLOOM_MN_EXT_*` env vars (see "Cluster hand-off"). This CLI
+ drives an **already-running** cluster and never creates or tears one down; the
+ platform reclaims it when the session ends.
- Bypassing loses: idempotency, `ownerId` cascade cleanup, exit-2 +
- `MULTI_NODE_FAILURE_SNAPSHOT={...}` failure detection, cross-subcommand
- state, and `BENCHMARK_BASE_URL` plumbing for Magpie.
+ Drive every action through the Python CLI. **Never `ray.init`, `kubectl`, or
+ raw `curl` to a pod.** All state lives in the file resolved from
+ `$MULTI_NODE_STATE_FILE`, else
+ `$INFERENCE_OPTIMIZER_CURRENT_SESSION_DIR/runtime/multi_node_state.json` — one
+ of the two MUST be set or the CLI raises (sandbox-local; synthesized from the
+ hand-off env on first use, rewritten by each subcommand). Re-read it
+ every turn — never cache `service_url` / `head_pod_ip` across actions (a
+ re-provisioned cluster rewrites them).
- ## Infera backend (`--mn-backend infera`)
+ | Action | Use | Never |
+ |----------------|-------------------|-------------------------------------------|
+ | Restart server | `restart-server` | `kubectl exec … sglang.launch_server` |
+ | Kill server | `kill-inference` | `pkill -f sglang` |
- Alternative multi-node backend: same "long-lived idle pod + external server
- restart" loop as RayJob, but on a SaFE **InferaDeployment** with **SSH** as the
- control plane instead of the Ray Dashboard. Only active when `--nodes >= 2`;
- single-node runs are unaffected. Select via `optimize --mn-backend infera`
- (or `$INFERENCE_OPTIMIZER_MN_BACKEND=infera`).
+ Bypassing loses exit-2 + `MULTI_NODE_FAILURE_SNAPSHOT={…}` failure detection,
+ cross-subcommand state, and `BENCHMARK_BASE_URL` plumbing for Magpie.
- Worker pods deploy **idle** (`mn-idle.sh` → sshd + block); `restart-server`
- SSHes in to (re)launch `infera.engine.sglang`/`infera.engine.vllm`, so the aiter JIT cache
- survives across restarts. Benchmarks always target the **Infera frontend
- :8000** (`state.service_url` → `BENCHMARK_BASE_URL`), never sglang rank-0 :8888.
+ ## Backends
- Requirements & behaviour:
+ Select via `optimize --mn-backend {infera|rayjob}` (default `rayjob`) or
+ `$INFERENCE_OPTIMIZER_MN_BACKEND`. Only active when `--nodes >= 2`; single-node
+ runs are unaffected. `restart-server` / `kill-inference` auto-route by
+ `state.backend`.
- * **Image** must carry the sshd layer (`docker/infera/Dockerfile.sshd`); sshd
- runs on `$MN_SSH_PORT` (default base 2222, not 22). Under hostNetwork, each
- GPU **role** binds a distinct port (prefill/worker `2222+N`, decode
- `2232+N` via `LWS_WORKER_INDEX`) so co-located roles on one node do not
- collide.
+ **infera** — `InferaDeployment` with **SSH** as the control plane. GPU pods
+ deploy idle (`mn-idle.sh` → sshd + block); `restart-server` SSHes in to
+ relaunch `infera.engine.{sglang,vllm}`, so the aiter JIT cache survives
+ restarts. No `bootstrap` / `verify` step. Benchmarks target the **Infera
+ frontend `:8000`** (`state.service_url`), never sglang rank-0.
+ * Image must carry the sshd layer (`docker/infera/Dockerfile.sshd`); sshd runs
+ on `$MN_SSH_PORT` (base **2233**, not 22 — avoids colliding with node sshd
+ on :2222). Under hostNetwork each GPU role binds a distinct port —
+ prefill/worker `2233+N`, decode `2243+N` (via `LWS_WORKER_INDEX`) — so
+ co-located roles don't collide.
* **Aggregated** (default): `serviceRoles=[frontend, worker]`,
- `multinodeRoles=[worker]`, `worker.replica = nodes`.
- * **PD disaggregation**: pass `optimize --pd-mode disaggregated
- --pd-prefill-nodes N --pd-decode-nodes M [--pd-prefill-tp/--pd-decode-tp]`
- (same flags as the RayJob backend). Produces `serviceRoles=[frontend,
- prefill, decode]`; a role becomes a LeaderWorkerSet (multi-node) only when
- its TP exceeds one pod's GPUs, otherwise its replica is independent
- single-node instances.
-
- Subcommands (`restart-server` / `kill-inference` auto-route by `state.backend`;
- no `init-env` / `verify` step):
-
- ```bash
- python3 -m hyperloom.inference_optimizer.multi_node create-infera --image <img-with-sshd> --model <path-or-hf-id> --nodes <N> \
- [--pd-mode disaggregated --pd-prefill-nodes N --pd-decode-nodes M] [--kv-transfer-backend mooncake]
- python3 -m hyperloom.inference_optimizer.multi_node restart-server --framework sglang --model <path> --tp <N> [--ep <N>] [--extra-args "..."]
- python3 -m hyperloom.inference_optimizer.multi_node kill-inference
- python3 -m hyperloom.inference_optimizer.multi_node stop-multi-job [--clear-state]
- ```
+ `worker.replica = nodes`.
+ * **PD-disaggregated**: `--pd-mode disaggregated --pd-prefill-nodes N
+ --pd-decode-nodes M [--pd-prefill-tp/--pd-decode-tp]` →
+ `serviceRoles=[frontend, prefill, decode]`. A role becomes a multi-node
+ LeaderWorkerSet only when its TP exceeds one pod's GPUs.
- Native params via `restart-server --extra-args` (standard sglang knobs:
- `--ep-size`, `--enable-dp-attention`, `--attention-backend aiter`,
- `--mem-fraction-static`) and `infera.server --router-mode {round-robin,kv}`.
- `--kv-transfer-backend {nixl,mori,mooncake}` selects the PD KV plane.
- Prefer `mooncake` for sglang on this RoCE/bnxt fabric: `nixl` completes
- requests with HTTP 200 but produces 0 output tokens (prefill OK, decode emits
- nothing — KV handoff via UCX/nixl fails to register/transfer). `mooncake`
- auto-detects the RDMA device and is the sglang framework default.
+ **rayjob** — `RayJob` driven via the **Ray Dashboard** (`:8265` submit, `:6379`
+ GCS); no SSH. Uses `bootstrap` → `verify` → `restart-server` (below).
- Kernel-agent on the Infera backend (no Ray): GEAK runs on a GPU pod over SSH
- (`KERNEL_AGENT_GPU_PLACEMENT=ssh`, injected only when `backend==infera`);
- `apply-patch` / `revert-patch` / `kernel-bench` fan out over SSH (routed by
- `state.backend`). The provisioner installs GEAK on the pods once
- (`install-geak`, from the shared `$HYPERLOOM_ROOT/geak` checkout) — skipped
- under `--no-kernel`. vLLM multi-node bootstraps Ray across the pods pod-side.
+ The topology (aggregated vs PD, per-role TP/EP, image, RDMA) is fixed by the
+ platform at provision time; the flags here only describe what `restart-server`
+ launches on the pods it was handed.
- ## External mode (SaFE-less: env-provided cluster)
+ ## Cluster hand-off (env-provided)
- When SaFE is **unavailable** (`SAFE_API_URL` / `SAFE_API_KEY` not both set) and
- `HYPERLOOM_MN_EXT_SERVICE_URL` is set, the optimizer **skips all SaFE
- create/init** and synthesizes the multi-node state from env vars, then
- benchmarks (and, when SSH/head is supplied, restarts + GPU-samples) an
- already-provisioned cluster. When both `SAFE_API_*` are present these external
- vars are ignored (normal SaFE flow).
+ `HYPERLOOM_MN_EXT_SERVICE_URL` is what marks a cluster as available: without it
+ a `--nodes >= 2` run has nothing to drive and exits 2.
- **Common (both backends):**
+ **Common (both backends)**
| Env var | Req? | Purpose |
| --- | --- | --- |
- | `HYPERLOOM_MN_EXT_SERVICE_URL` | **yes** | HTTP(S) frontend for benchmarks (-> `BENCHMARK_BASE_URL`); presence triggers external mode |
+ | `HYPERLOOM_MN_EXT_SERVICE_URL` | **yes** | HTTP(S) frontend for benchmarks (→ `BENCHMARK_BASE_URL`); its presence marks the cluster available |
- **Infera backend (`--mn-backend infera`):**
+ **infera (`--mn-backend infera`)**
| Env var | Req? | Purpose |
| --- | --- | --- |
- | `HYPERLOOM_MN_EXT_SSH_KEY` | **yes** | private key that can SSH into the pods (you supply it; no SaFE to inject one) |
- | `HYPERLOOM_MN_EXT_PREFILL_IPS` / `_DECODE_IPS` / `_WORKER_IPS` | **yes** (at least one) | comma-separated GPU pod IPs (topology / PD / GPU sampling). PD-disaggregated uses `_PREFILL_IPS` + `_DECODE_IPS`; aggregated uses `_WORKER_IPS` |
- | `HYPERLOOM_MN_EXT_SSH_PORT` | no | SSH base port (default 2233; decode is role-offset +10) |
+ | `HYPERLOOM_MN_EXT_SSH_KEY` | **yes** | path to a private key already authorised on the pods (platform writes the file + bakes its public half at pod-create; never refreshed) |
+ | `HYPERLOOM_MN_EXT_PREFILL_IPS` / `_DECODE_IPS` / `_WORKER_IPS` | **yes** (≥1) | comma-separated GPU pod IPs. PD uses `_PREFILL_IPS` + `_DECODE_IPS`; aggregated uses `_WORKER_IPS` |
+ | `HYPERLOOM_MN_EXT_SSH_PORT` | no | SSH base port (default **2233**; decode role-offset +10) |
| `HYPERLOOM_MN_EXT_SSH_KNOWN_HOSTS` | no | known_hosts path (else lax host-key check) |
- **RayJob backend (`--mn-backend rayjob`):**
+ infera **requires** SSH (`_SSH_KEY` + ≥1 `*_IPS`); missing → fails fast
+ (**exit 3**, config error), no degrade.
+ Which list is read is decided by `$PD_MODE` alone, never guessed from which
+ lists you set: unset means aggregated, so a PD cluster that exports
+ `_PREFILL_IPS` / `_DECODE_IPS` without `PD_MODE=disaggregated` reads as having
+ no GPU pods at all. `optimize` exports it from `--pd-mode`; a standalone
+ `hyperloom-mn` subcommand needs it in the environment.
+
+ **rayjob (`--mn-backend rayjob`)** — ignores the infera `_SSH_*` / `*_IPS` vars.
+
| Env var | Req? | Purpose |
| --- | --- | --- |
- | `HYPERLOOM_MN_EXT_HEAD_IP` | recommended | Ray head pod IP -> Dashboard `:8265` (job submit) + GCS `:6379` (derived `ray_address`). Enables per-round `restart-server` via Ray. Omit for **benchmark-only** (no restarts) |
- | `HYPERLOOM_MN_EXT_RAY_DASHBOARD_TOKEN` | no | Ray Dashboard auth token (only if the dashboard is authenticated) |
-
- RayJob external does **not** use SSH; the infera `_SSH_*` / `*_IPS` vars are
- ignored for `--mn-backend rayjob`.
+ | `HYPERLOOM_MN_EXT_HEAD_IP` | recommended | Ray head IP → Dashboard `:8265` + GCS `:6379`; enables per-round `restart-server`. Omit ⇒ **benchmark-only** (restarts no-op). Must be the `<rayCluster>-head-svc`, not the workload's single-port Service |
+ | `HYPERLOOM_MN_EXT_RAY_DASHBOARD_TOKEN` | no | Dashboard auth token (only if authenticated) |
- **Companion vars (reused as-is, normally set by `optimize` from CLI flags):**
+ **Companion vars** (set by `optimize` from CLI flags; usually leave alone):
`INFERENCE_OPTIMIZER_NODES`, `INFERENCE_OPTIMIZER_GPUS_PER_NODE`, `PD_MODE`,
- `PD_PREFILL_NODES` / `PD_DECODE_NODES` (inferred from IP-list length when unset),
- `INFERENCE_OPTIMIZER_MN_BACKEND`, `SAFE_WORKSPACE` (passthrough).
-
- Behavior:
-
- * **infera external REQUIRES SSH** (`_SSH_KEY` + at least one `*_IPS`); if missing
- the run **fails fast** (`sys.exit(2)`) rather than degrading. With SSH: full
- SSH restart + on-pod GPU sampling + pd/by-role telemetry, exactly like a
- SaFE-created infera deployment. The SSH keypair is normally hyperloom-generated
- and its **public** key is injected into the pods by SaFE at create time; with
- SaFE absent you pre-authorize your own key on the pods and pass its private path
- via `_SSH_KEY`.
- * **rayjob external** uses Ray via `_HEAD_IP` (not SSH); Dashboard on `:8265`,
- GCS on `:6379`. With `_HEAD_IP`: per-round `restart-server` via Ray job submit.
- Without `_HEAD_IP`: **benchmark-only** (per-round restart no-ops). The infera
- SSH rule does not apply.
+ `PD_PREFILL_NODES` / `PD_DECODE_NODES`, `INFERENCE_OPTIMIZER_MN_BACKEND`.
- Example (SaFE assumed absent, infera PD-disaggregated):
+ Example (infera PD-disaggregated):
```bash
- unset SAFE_API_URL SAFE_API_KEY
export HYPERLOOM_MN_EXT_SERVICE_URL=http://<frontend-host>:8000
- export HYPERLOOM_MN_EXT_PREFILL_IPS=<prefill-ip> HYPERLOOM_MN_EXT_DECODE_IPS=<decode-ip>
+ export HYPERLOOM_MN_EXT_PREFILL_IPS=<prefill-ip> HYPERLOOM_MN_EXT_DECODE_IPS=<decode-ip>
export HYPERLOOM_MN_EXT_SSH_KEY=/path/to/id_ed25519
- export INFERENCE_OPTIMIZER_NODES=2 PD_MODE=disaggregated
inference_optimizer optimize --model <path> --nodes 2 \
--mn-backend infera --pd-mode disaggregated --tp 8 --ep 8 ...
```
- Example (SaFE assumed absent, rayjob with per-round restart):
+ Example (rayjob with per-round restart):
```bash
- unset SAFE_API_URL SAFE_API_KEY
export HYPERLOOM_MN_EXT_SERVICE_URL=http://<ray-serve-or-head-url>:<port>
- export HYPERLOOM_MN_EXT_HEAD_IP=<ray-head-ip>
- # optional: export HYPERLOOM_MN_EXT_RAY_DASHBOARD_TOKEN=<token>
- export INFERENCE_OPTIMIZER_NODES=2
- inference_optimizer optimize --model <path> --nodes 2 \
- --mn-backend rayjob --tp 8 --ep 8 ...
+ export HYPERLOOM_MN_EXT_HEAD_IP=<ray-head-svc-host>
+ inference_optimizer optimize --model <path> --nodes 2 --mn-backend rayjob --tp 8 --ep 8 ...
```
- ## The Five Subcommands
+ ## Subcommands
```bash
- python3 -m hyperloom.inference_optimizer.multi_node create-rayjob --image <rayjob-image> --nodes <N>
- python3 -m hyperloom.inference_optimizer.multi_node init-env [--print-logs]
- python3 -m hyperloom.inference_optimizer.multi_node verify
- python3 -m hyperloom.inference_optimizer.multi_node restart-server --framework <sglang|vllm> --model <path-or-id> --tp <N> [--extra-args "..."]
- python3 -m hyperloom.inference_optimizer.multi_node stop-multi-job [--clear-state]
+ # rayjob only — infera skips these two:
+ python3 -m hyperloom.inference_optimizer.multi_node bootstrap [--print-logs] # verify /opt/venv + write PATH env on head
+ python3 -m hyperloom.inference_optimizer.multi_node verify # check `ray` on PATH on head
+ # both backends:
+ python3 -m hyperloom.inference_optimizer.multi_node restart-server --framework <sglang|vllm> --model <path> --tp <N> [--ep <N>] [--extra-args "…"]
+ python3 -m hyperloom.inference_optimizer.multi_node kill-inference
```
- Run `<subcommand> --help` for the full flag set. **Do not invent flags.**
-
- ### Parameter sources
-
- * **From sandbox env** (do NOT pass on CLI / read from prompt):
- `SAFE_API_URL`, `SAFE_API_KEY`, `SAFE_WORKSPACE`, `DISPLAY_NAME`,
- `WORKLOAD_ID` — Brain / SaFE injects at sandbox start.
- * **From user prompt** (verbatim): `--image` ← `RayJob image:`;
- `--nodes` ← `Nodes=N` / multi-node phrasing; `--cpus-per-node` /
- `--mem-per-node` / `--ephemeral-per-node` ← `RayJob resource:`;
- `--tp` (restart-server only) ← `TP=N`; `--extra-env KEY=VAL` (repeatable)
- ← prompt `env:` block (skip `*_API_KEY` / `*_BASE_URL` /
- `RAY_JOB_ENTRYPOINT` — auto-injected).
- * **Defaults** (omit when prompt is silent): `--workspace`→`$SAFE_WORKSPACE`,
- `--gpus-per-node`→`8`, `--display-name`→`$DISPLAY_NAME` else
- generated (see `DISPLAY_NAME` section below), `--owner-id`→`$WORKLOAD_ID`.
+ Run `<subcommand> --help` for the full flag set. **Do not invent flags.** There
+ is no create/stop subcommand. Issue ONE `restart-server` per framework / model /
+ TP / flag change — the CLI fans out across all pods; never per-pod invocations.
- ### Map the user Environment block → CLI (do not re-ask)
+ > **If you are running the `optimize` CLI** (handed `--target-gain` /
+ > `--max-hours` / `--isl/--osl/--conc` FLAGS), `optimize` performs the entire
+ > flow internally (adopt cluster → bootstrap → restart per round). Run
+ > **`optimize` ONLY** — do not also run standalone `bootstrap` / `restart-server`
+ > alongside it, or the two resolve different state files and fight over the pods.
+ > The manual subcommands are only for driving a cluster *without* `optimize`.
- When the user prompt already lists topology / workload / kernel knobs, the
- launcher **maps** them — it does not need the user to repeat them in chat.
- Typical prompt fields and where they land:
+ ### Prompt → CLI mapping (do not re-ask; do not repeat in chat)
- | User prompt | Launcher action |
+ | User prompt field | Launcher action |
|---|---|
- | `Nodes=N` / `N nodes` | `create-rayjob --nodes N`; `optimize --nodes N` |
- | `RayJob image: …` / `Infera image: …` | `create-rayjob`/`create-infera --image …`; `optimize --mn-image …` |
- | `TP=N`, `EP=…` | `restart-server --tp N`; `optimize --tp` / `--ep`. **Always set `--tp`** (default is 1); for PD set it to the per-role TP. |
- | `MN_BACKEND=infera` | `optimize --mn-backend infera` (selects the idle InferaDeployment + SSH backend; default `rayjob`). |
- | `PD_MODE=disaggregated` | `optimize --pd-mode disaggregated` — **must be passed as a flag**; `$PD_MODE` env is deliberately ignored (stale-env guard). Omit ⇒ aggregated. |
- | `PD_PREFILL_NODES` / `PD_DECODE_NODES` | `optimize --pd-prefill-nodes N --pd-decode-nodes M` (or export `$PD_PREFILL_NODES`/`$PD_DECODE_NODES` — read as flag defaults). |
- | `PD_PREFILL_TP` / `PD_DECODE_TP` | `optimize --pd-prefill-tp N --pd-decode-tp M` (or export; default = `--tp`). A PD role spans nodes (LWS) only when its TP > GPUs-per-pod. |
- | `PD_PREFILL_EP` / `PD_DECODE_EP` | **Infera PD only.** export `$PD_PREFILL_EP` / `$PD_DECODE_EP` (read by `restart-server` as defaults). Per-role expert-parallel size; `0` (default) ⇒ fall back to the shared `--ep`. Lets prefill run EP1 while decode runs EP8 (InferenceX disagg recipe). Ignored by RayJob/aggregated/single-node. |
- | `PD_PREFILL_EXTRA_ARGS` / `PD_DECODE_EXTRA_ARGS` | **Infera PD only.** export these; appended to the **per-role** sglang launch AFTER the shared `--extra-args` base (role-specific wins on duplicate keys). Used to give prefill vs decode different server flags (e.g. decode `--enable-dp-attention --moe-a2a-backend deepep --deepep-mode normal --moe-dense-tp-size 1 --enable-dp-lm-head`; prefill `--mem-fraction-static 0.8 --disable-radix-cache`). Empty (default) ⇒ both roles use only the shared `--extra-args`. **Sandbox-only** (do NOT `--rayjob-extra-env`). |
- | `PD_TRANSFER_BACKEND` | `optimize --pd-transfer-backend mooncake` (or export `$PD_TRANSFER_BACKEND`); `nixl|mori|mooncake`. **Use `mooncake` for sglang** — `nixl` returns 200 OK but 0 output tokens on this RoCE/bnxt fabric (decode KV handoff fails). |
- | `ISL` / `OSL` / `CONC` / `PRECISION` | `export` + `optimize --isl` / `--osl` / `--conc` / `--precision` |
- | `KERNEL_OPT_*` / `KERNEL_AGENT_BUILD_GEAK_RAG_INDEX` | `export` before `install.sh` / `optimize` |
- | prompt `env:` block lines (e.g. `PATH_TO_AINIC_TAR_PACKAGE=…`, `PATH_TO_BNXT_TAR_PACKAGE=…`, `NCCL_DEBUG=INFO`) | `create-rayjob --extra-env K=V` (one per line, repeatable); `optimize --rayjob-extra-env K=V` (same shape). Skip `*_API_KEY` / `*_BASE_URL` (credential fanout auto-injects) and `RAY_JOB_ENTRYPOINT` (reserved). CLI owns no defaults — values come verbatim from the prompt. **Do NOT forward sandbox-side tool source fields** (`OOB_SRC` / `INFERENCEX_PATH` / `TRACELENS_ROOT`) here — they are sandbox-only; see `inference_optimizer/SKILL.md` "Tool source fields". |
- | MoE JIT cold-start (often omitted in prompt) | `export HYPERLOOM_MN_POLL_TIMEOUT_S=1800` and `HYPERLOOM_MN_HEALTH_WAIT_S=1800` — see below |
-
- If the prompt already contains the first rows, **do not** claim the
- “environment block is incomplete”; wire them into `setsid nohup optimize`
- and `multi_node` subcommands. Only add exports the prompt did not cover
- (chiefly `HYPERLOOM_MN_*` for 30 min polls on large MoE RayJobs).
-
- **DO NOT `--rayjob-extra-env` these (sandbox-only):**
-
- These are consumed by `install.sh` / `python -m hyperloom.inference_optimizer.cli optimize` /
- `_workload_envs.py` running inside the **sandbox**; nothing inside the
- RayJob pod reads them. Forwarding them pollutes the pod env and risks
- shadowing real values.
-
- - `KERNEL_AGENT_BUILD_GEAK_RAG_INDEX`, `KERNEL_OPT_*`
- - `RANDOM_RANGE_RATIO`, `RUN_EVAL`
- - `MODEL_PATH`, `FRAMEWORK`, `TP`, `EP`, `ISL`, `OSL`, `CONC`, `PRECISION`,
- `TARGET_GAIN`, `MAX_HOURS`, `GPU_TYPE`, `NODES` (already passed as
- `optimize` CLI flags)
- - `MN_BACKEND`, `PD_MODE`, `PD_PREFILL_NODES`, `PD_DECODE_NODES`,
- `PD_PREFILL_TP`, `PD_DECODE_TP`, `PD_PREFILL_EP`, `PD_DECODE_EP`,
- `PD_PREFILL_EXTRA_ARGS`, `PD_DECODE_EXTRA_ARGS`, `PD_TRANSFER_BACKEND`
- (sandbox-side `optimize` flags / env; the Infera deployment is created by
- `create-infera` from these / consumed by `restart-server`, NOT injected
- into pods)
- - `HYPERLOOM_MN_POLL_TIMEOUT_S`, `HYPERLOOM_MN_HEALTH_WAIT_S` (sandbox
- CLI poll budget, not a pod env)
-
- Forward to `--rayjob-extra-env` **only** the prompt `env:` block lines
- (`NCCL_DEBUG`, `PATH_TO_*` etc.). `OOB_SRC` / `INFERENCEX_PATH` /
- `TRACELENS_ROOT` are sandbox-only and **must NOT** be forwarded (the
- RayJob pod does not consume them — kernel-bench is NodeAffinity-pinned
- to the head pod).
-
- Example `optimize` tail (**example only** — map each flag from the user
- Environment block / `setup_env.sh`; do not treat literals below as defaults):
-
- ```bash
- # Multi-node poll budget (large MoE RayJobs; see "MoE JIT poll budget" below)
- export HYPERLOOM_MN_POLL_TIMEOUT_S=1800
- export HYPERLOOM_MN_HEALTH_WAIT_S=1800
- # Optional kernel exports — only when the prompt specifies them
- export KERNEL_OPT_BACKEND_ORDER="${KERNEL_OPT_BACKEND_ORDER:-claude}"
- export KERNEL_AGENT_BUILD_GEAK_RAG_INDEX="${KERNEL_AGENT_BUILD_GEAK_RAG_INDEX:-0}"
-
- setsid nohup python3 -m hyperloom.inference_optimizer.cli --verbose optimize \
- --model "$MODEL_PATH" \
- --framework "${FRAMEWORK:-sglang}" \
- --gpu-type "${GPU_TYPE:?set from prompt}" \
- --nodes "${NODES:?set from prompt Nodes=N}" \
- --mn-image "${INFERENCE_OPTIMIZER_MN_IMAGE:?set from prompt multi-node image}" \
- --tp "${TP:?set from prompt TP=N}" \
- ${EP:+--ep "$EP"} \
- --conc "${CONC:?set from prompt}" \
- --isl "${ISL:?set from prompt}" \
- --osl "${OSL:?set from prompt}" \
- --precision "${PRECISION:?set from prompt}" \
- --target-gain "${TARGET_GAIN:?set from prompt}" \
- --max-hours "${MAX_HOURS:?set from prompt}" \
- ${KERNEL_CLAUDE:+--kernel-claude} \
- ${CLAUDE_MODEL:+--claude-model "$CLAUDE_MODEL"} \
- $(for kv in "${RAYJOB_EXTRA_ENV[@]:-}"; do [ -n "$kv" ] && printf -- '--rayjob-extra-env %q ' "$kv"; done) \
- > "$RUN_LOG" 2>&1 < /dev/null &
- ```
-
- ### `DISPLAY_NAME` (SaFE workload create only)
+ | `Nodes=N` | `optimize --nodes N` (must match the handed-over cluster) |
+ | `TP=N`, `EP=…` | `optimize --tp N` / `--ep`; `restart-server --tp N`. **Always set `--tp`** (default 1); for PD use the per-role TP |
+ | `MN_BACKEND=infera` | `optimize --mn-backend infera` |
+ | `PD_MODE=disaggregated` | `optimize --pd-mode disaggregated` — **must be a flag**; `$PD_MODE` env is ignored (stale-env guard). Omit ⇒ aggregated |
+ | `PD_PREFILL_NODES` / `PD_DECODE_NODES` | `--pd-prefill-nodes` / `--pd-decode-nodes` (or export) |
+ | `PD_PREFILL_TP` / `PD_DECODE_TP` | `--pd-prefill-tp` / `--pd-decode-tp` (default = `--tp`) |
+ | `PD_PREFILL_EP` / `PD_DECODE_EP` | **infera PD only**, export as `restart-server` defaults; `0` ⇒ shared `--ep` |
+ | `PD_PREFILL_EXTRA_ARGS` / `PD_DECODE_EXTRA_ARGS` | **infera PD only**, export; appended per-role AFTER shared `--extra-args` (role wins on dup keys) |
+ | `PD_TRANSFER_BACKEND` | `--pd-transfer-backend {nixl\|mori\|mooncake}`. **Use `mooncake` for sglang** (see below) |
+ | `ISL`/`OSL`/`CONC`/`PRECISION` | `optimize --isl/--osl/--conc/--precision` |
+ | `--mn-image …` / `RayJob image:` / `Infera image:` | **Not an `optimize` flag** — the image is the platform's input; pods already exist by the time the optimizer runs |
+ | prompt `env:` lines (`NCCL_DEBUG`, `PATH_TO_*_TAR_PACKAGE`, …) | Pod env is baked by the platform at provision time — pass to the platform, not this CLI |
- Only `create-rayjob` sets the SaFE workload name. Resolution order:
+ **Sandbox-only exports** (consumed by `install.sh` / `optimize` /
+ `_workload_envs.py` in the **sandbox**; never ask the platform to bake them into
+ pod env — nothing in the pods reads them, and they shadow real pod values):
+ `KERNEL_AGENT_BUILD_GEAK_RAG_INDEX`, `KERNEL_OPT_*`, `RANDOM_RANGE_RATIO`,
+ `RUN_EVAL`, the `optimize`-flag mirrors (`MODEL_PATH`, `TP`, `EP`, …), all
+ `PD_*` knobs, and `HYPERLOOM_MN_POLL_TIMEOUT_S` / `HYPERLOOM_MN_HEALTH_WAIT_S`.
- 1. If `$DISPLAY_NAME` is set, use it as-is — do **not** pass `--display-name`.
- 2. Otherwise generate one that satisfies the SaFE admission webhook:
- length **1–36**, lowercase letters / digits / hyphens only (`[a-z0-9-]`),
- start with a letter, end with alphanumeric. Good: `hl-run-$(date +%m%d%H%M)`.
- Bad: `hyperloom-sglang-2node-20260522_022937` (too long / underscores).
+ ### KV transfer backend (PD)
- ### SaFE workload `phase` (source of truth)
+ `--pd-transfer-backend` selects the PD KV plane, on `optimize` or directly on
+ `restart-server`; `optimize` mirrors it to `$PD_TRANSFER_BACKEND`, which later
+ rounds fall back on. There is no state-file source: the cluster is handed over
+ rather than created here, so nothing records a backend on your behalf.
- `create-rayjob` polls **SaFE GetWorkload `phase`** until it is **`Running`**.
- Do **not** treat individual pod `phase=Running` as a substitute while the
- workload is still `Pending` — init-env / `head_pod_ip` / benchmarks must
- wait for the workload object to flip. If poll times out with `phase=Pending`,
- re-run the **same** `create-rayjob` (idempotent resume) with a longer
- `HYPERLOOM_MN_POLL_TIMEOUT_S` or `--poll-timeout`; inspect
- `conditions` / `message` on the workload for queue or dispatch issues.
+ Leaving it unset is fine, and usually right. The flag is then simply not passed
+ and sglang applies its own default, which on the RoCE/bnxt fabric is `mooncake`
+ -- the one to **prefer** here, since it auto-detects the RDMA device. Set the
+ flag only to move off that: `nixl` returns HTTP 200 but 0 output tokens (decode
+ KV handoff via UCX/nixl fails), and `mori` is an alternative but not the sglang
+ default here.
- ### MoE JIT poll budget (110s default is too short)
+ ### Poll budget (MoE JIT cold-start)
- First `restart-server` on a multi-node RayJob often needs **20–30 minutes**
- (weight load + aiter JIT). The default per-invocation poll is **~110s**
- (ADDENDUM-09). Export before `restart-server` / `optimize --nodes >=2`:
+ First `restart-server` on a large MoE often needs **20–30 min** (weight load +
+ aiter JIT), but the default per-invocation poll is ~110s. Export before
+ `restart-server` / `optimize --nodes >= 2`:
```bash
export HYPERLOOM_MN_POLL_TIMEOUT_S=1800
export HYPERLOOM_MN_HEALTH_WAIT_S=1800
```
- On timeout, re-run the **same** subcommand (no `while sleep` wrapper).
- `restart-server` checkpoints `last_restart_submission_id` so retries can
- resume an in-flight launch (`MULTI_NODE_RESTART_RESUME_RUNNING=1`, default).
-
- ## Call Order
-
- > **STOP — are you running the `optimize` CLI?** If the task runs
- > `python -m hyperloom.inference_optimizer.cli optimize --nodes N>=2
- > --mn-backend rayjob ...` (i.e. you were handed `optimize`-style FLAGS
- > like `--target-gain` / `--max-hours` / `--isl/--osl/--conc`), then
- > **`optimize` performs this ENTIRE Call Order internally** — it
- > provisions/reuses the RayJob, bootstraps, and restarts the server per
- > round. In that mode **run `optimize` ONLY**; do **NOT** also run a
- > standalone `create-rayjob` / `init-env` / `restart-server` first or
- > alongside it. Doing so provisions a SECOND RayJob (the standalone CLI
- > and `optimize`'s in-process provisioning may resolve different state
- > files and each create their own workload), wasting a full node set and
- > deadlocking scheduling. The manual steps below are ONLY for driving the
- > RayJob lifecycle *without* `optimize`.
-
- 1. **`create-rayjob`** — once. Persists `rayjob_id` before polling
- (overlapping retries never spawn a second RayJob), then fills
- `head_pod_ip` / `service_url` once phase is `Running`.
- 2. **`init-env`** — once. Submits `init_rayjob_env.sh` via Ray Dashboard REST
- to verify `/opt/venv` and write `hyperloom-env.sh` (PATH only) on the
- head pod.
- 3. **`verify`** — once. Checks `ray` on PATH on the head pod.
- On `MISSING:`, re-run `init-env --print-logs`.
- 4. **`restart-server`** — every framework / model / TP / flag change.
- Kills the previous server via PID file (never `pkill -f`), relaunches
- under `nohup` so Ray pods do NOT restart and the aiter JIT cache
- survives. Issue ONE invocation per change; the CLI fans out across
- all pods on multi-node runs. Never issue per-pod invocations.
- 5. **`stop-multi-job`** — at session end. Always call explicitly for an
- auditable release. `ownerId` cascade is a safety net (sandbox
- deletion removes the SaFE workload and tears the RayJob down via
- owner-ref), not a substitute. `python -m hyperloom.inference_optimizer.cli optimize
- --nodes N>=2` does **not** call it on exit; nor does sandbox idle /
- hard-TTL GC distinguish "session in progress" from "session
- abandoned" — when the sandbox dies the RayJob is collateral, so an
- in-flight optimize loses access to head_pod_ip / service_url even
- if RayJob teardown lags a few seconds behind sandbox pod removal.
-
- After step 4 route all benchmark / Magpie traffic to
- `state.service_url` (head pod ClusterIP `:8888`). Re-read
- `$MULTI_NODE_STATE_FILE` every turn; never cache `head_pod_ip` /
- `service_url` across actions — RayJob recreate (or `stop-multi-job` then
- `create-rayjob` again) reassigns the head pod and rewrites both keys.
+ On timeout, re-run the **same** subcommand (no `while sleep` wrapper);
+ `restart-server` checkpoints `last_restart_submission_id` and resumes the
+ in-flight launch (`MULTI_NODE_RESTART_RESUME_RUNNING=1`, default).
## Hard Rules
- * **ADDENDUM-09** (bash budget): each CLI invocation polls until
- `--poll-timeout` (default 110s unless `HYPERLOOM_MN_POLL_TIMEOUT_S`
- is set) then exits. For MoE JIT cold-start on RayJob pods, export
- `HYPERLOOM_MN_POLL_TIMEOUT_S=1800` and `HYPERLOOM_MN_HEALTH_WAIT_S=1800`
- before `restart-server` / `optimize --nodes >=2`. Timeout → rerun the
- **same** subcommand (resume uses `last_restart_submission_id`). Never
- wrap in `sleep` / `while true ...; sleep 60; done`.
- * **ADDENDUM-13** (credentials): `SAFE_API_URL` / `SAFE_API_KEY` must be
- in sandbox env at CLI start (Brain injects). LLM keys are consumed in the
- sandbox only — not via `create-rayjob` workload env or `init_rayjob_env.sh`.
- **Never pass keys on the command line.**
- * **ADDENDUM-02** (no Ray Python client in orchestration layer):
- `multi_node/cli.py` and `multi_node/_internal/` MUST use Ray Dashboard
- REST only — never `import ray` / `ray.init(address=...)` against the
- inference RayJob. (`pip install ray` in sandbox is fine for unrelated
- stacks. Code submitted as a Ray Dashboard entrypoint and running
- inside RayJob pods is exempt — those pods *are* the Ray cluster.)
- * **ADDENDUM-14** (sandbox never runs the inference server): When
- `nodes >= 2`, sglang / vllm lives only on RayJob pods; the sandbox is
- the client. Every Magpie launch MUST inherit
- `BENCHMARK_BASE_URL=<state.service_url>` (forces `PHASE=client`).
- Missing it → Magpie defaults to `PHASE=all` → `python3 -m
- sglang.launch_server` on the CPU sandbox → `ModuleNotFoundError`.
- Always fix orchestrator env propagation; never `pip install sglang`
- in the sandbox. (`/etc/profile.d/hyperloom.sh` global export is a
- shell-level backstop only — head pod IP changes per RayJob recreate.)
- * **ADDENDUM-15** (kernel-agent fan-out): kernel-agent runs in the
- sandbox but writes to source under `/sgl-workspace/{aiter,sglang,vllm}/`
- which is per-pod local fs — sandbox edits do NOT reach the RayJob
- pods. Use the three multi-node subcommands instead:
- * `apply-patch` — fan-out a kernel patch to every pod (head + workers)
- via `kernel_patch_multinode.py`; per-host backups are written under
- `--backup-dir` and returned to the caller for revert.
- * `revert-patch` — inverse of apply, takes the per-host backup map.
- * `kernel-bench` — run a kernel micro-benchmark on a GPU-bearing pod
- (the sandbox is CPU-only in multi-node mode); stages helper files
- + bench script onto the pod, runs `bash --bench-command`, reads
- back result artifacts. Used by kernel_optimization.py prompt
- template; not invoked directly by the agent.
- Integrate path auto-restarts the server after `apply-patch`
- (bypassing the resume fast-path) and RayJob recreate auto-replays
- applied patches — do not invoke either step manually.
- * **ADDENDUM-16** (robustness LocalProbe is sandbox-scoped): the
- `robustness-agent` backend's `LocalProbeSource` family probes
- sandbox-local resources only — `ray status`, the inference server
- health URL (`http://127.0.0.1:8888`), GPU / FD / disk / shm metrics,
- the local log-error scanner, etc. On `--nodes >= 2` every one of
- those resources lives in a separate Kubernetes pod (head pod /
- worker pod / RayJob submitter, on a different subnet from the
- sandbox in some clusters), so each probe surfaces as a HIGH-severity
- false positive (`ray_head_dead`, `local_server_unreachable`,
- `gpu_memory_leaked`, ...). The CLI
- auto-downgrades `--robustness-agent` to `--robustness-mock`
- (heartbeat-only) when `args.nodes >= 2` and prints a WARNING.
- Operators who want to suppress the WARNING pass `--robustness-mock`
- explicitly. Until `robustness-agent` grows multi-node-aware probe
- targeting (probe head pod over the cluster service URL, route
- GPU / log probes through `kubectl exec` or a sidecar), the
- multi-node path keeps robustness on the mock heartbeat.
-
- ## Robustness limitation in multi-node mode
-
- `inference_optimizer.cli._resolve_robustness_choice` enforces the
- contract above:
-
- ```python
- # pseudo-code mirroring the actual logic
- if args.nodes >= 2 and chosen == "agent":
- if explicit:
- print("WARN: ... auto-downgrading to --robustness-mock ...",
- file=sys.stderr)
- chosen = "mock"
- ```
-
- Operator-visible effects:
-
- * All robustness intents are heartbeats — no `alert(HIGH)`,
- no `escalate_strategy_change`, no `delegate(report)` /
- `delegate(recover)` / `delegate(server_lifecycle)`,
- no `prune_branch`, no `kill_task`.
- * The `<session_dir>/robustness-workdir/` and
- `<session_dir>/agents/robustness/` directories stay empty (mock
- backend does not write them).
- * Long-run health monitoring still works at the **shell** level via
- `optimizer_runs/robustness_monitor.sh` (polls `state.json`,
- detects terminal `stop_reason`, auto-resumes a dead optimizer);
- that monitor is independent of the in-process robustness backend.
-
- The auto-downgrade is unconditional on `args.nodes >= 2`. The
- explicit-flag WARNING is the only operator signal (silent when the
- default `--robustness-agent` was selected via
- `DEFAULT_ROBUSTNESS_BACKEND` rather than an explicit CLI flag).
+ * **Sandbox never runs the inference server.** When `nodes >= 2`, sglang/vllm
+ live only on the pods; the sandbox is the client. Every Magpie launch MUST
+ inherit `BENCHMARK_BASE_URL=<state.service_url>` (forces `PHASE=client`).
+ Missing it → Magpie runs `sglang.launch_server` on the CPU sandbox →
+ `ModuleNotFoundError`. Fix orchestrator env propagation; never
+ `pip install sglang` in the sandbox.
+ * **Credentials stay in the sandbox** — never forward LLM keys into pod env /
+ `bootstrap.sh`, never pass keys on the command line.
+ * **No Ray Python client in the orchestration layer** — `multi_node/` uses Ray
+ Dashboard REST only, never `import ray` / `ray.init(address=…)` against the
+ inference RayJob. (Code running *inside* RayJob pods is exempt.)
+ * **Kernel-agent fan-out** — kernel edits in the sandbox do NOT reach the pods
+ (per-pod local fs). Use `apply-patch` / `revert-patch` / `kernel-bench` (route
+ by `state.backend`; on infera GEAK runs on a GPU pod over SSH, installed once
+ per cluster via `install-geak`). The integrate path auto-restarts the server
+ after `apply-patch` — do not restart manually.
+ * **Robustness auto-downgrades to mock on `nodes >= 2`.** The agent's
+ LocalProbe only sees sandbox-local resources, so on multi-node every probe
+ (`ray_head_dead`, `local_server_unreachable`, `gpu_memory_leaked`, …) is a
+ false positive. The CLI forces `--robustness-mock` (heartbeat-only; warns only
+ if `--robustness-agent` was explicit). Shell-level health monitoring
+ (`optimizer_runs/robustness_monitor.sh`, auto-resume on terminal
+ `stop_reason`) is unaffected.
- ## Exit Codes (for the controller / agent)
+ ## Exit Codes
- | Code | Meaning | Controller action |
- |-----:|----------------------------------------|------------------------------------------------|
- | 0 | success | continue |
- | 1 | transient (poll timeout / SaFE 5xx / | safe to rerun the SAME subcommand to retry |
- | | network error / unknown exception) | |
- | 2 | workload entered Failed/Stopped/Cancelled | DO NOT retry; cluster is unusable. Stderr |
- | | | also carries `MULTI_NODE_FAILURE_SNAPSHOT={...}` |
- | | | with the structured failure detail |
- | 3 | config error: SaFE 4xx (image not | DO NOT retry as-is; fix args / env and rerun |
- | | found, quota, missing workspace, bad | |
- | | label) / missing env / missing arg | |
- | 130 | SIGINT / Ctrl-C | user aborted |
+ | Code | Meaning | Controller action |
+ |-----:|---------|--------------------|
+ | 0 | success | continue |
+ | 1 | transient (poll timeout / network / unknown) | rerun the SAME subcommand |
+ | 2 | cluster unusable (no hand-off, infera without SSH, pods gone) | DO NOT retry; stderr may carry `MULTI_NODE_FAILURE_SNAPSHOT={…}` |
+ | 3 | config error (missing env / arg) | fix args/env, then rerun |
+ | 130 | SIGINT | user aborted |
## When Something Looks Wrong
- * `create-rayjob` times out → rerun; state already has `rayjob_id`,
- rerun resumes polling.
- * `init-env` fails → `init-env --print-logs` once for the trace.
- The script (`multi_node/scripts/init_rayjob_env.sh`) is repo-owned and
- should not be edited from the agent — failures usually point at one
- of: the RayJob image lacking `/opt/venv`, or the head pod failing to
- reach an upstream package / model registry. Fix the root cause (image /
- network), then rerun; RayJob stays alive across the retry.
- * `restart-server` hangs in health probe → rerun with `--no-wait-health`
- to detach, then `verify` / `curl state.service_url/health` to debug.
- * Cluster-side cleanup → `stop-multi-job --delete --clear-state` (hard
- delete = SaFE `DELETE /workloads/{id}`).
- * `ModuleNotFoundError: No module named 'sglang'` + stderr shows
- `setsid python3 -m sglang.launch_server` on the sandbox →
- ADDENDUM-14 trip. Fix orchestrator env propagation;
- `/etc/profile.d/hyperloom.sh` export is an interim unblock.
- * `restart-server` driver SUCCEEDED then `/health` wait timed out
- (default `DEFAULT_HEALTH_TIMEOUT_S = 900s`; override per-run via
- `HYPERLOOM_MN_HEALTH_WAIT_S`) → legacy launch_multinode swallowed
- framework early-exit. Patched: on rank-0 pid death the driver now
- exits 2 + writes
- `MULTI_NODE_FAILURE_SNAPSHOT={kind:"framework_early_exit",...}`; the
- Ray Dashboard job flips to FAILED and grid_runner skips the variant
- in seconds. Stderr `rank_0.log` tail names the cause.
-
- * **Variant silently aborts with no benchmark output** — read
- `failed_variants` inside the round's `<action>_attempts.extras`
- (Coordinator surfaces per-variant aborts there). For post-mortem
- forensics, the per-variant `abort_reason.json` written by
- grid_runner under
- `${USER_DATA_PATH}/runs/<action>/<task>/<variant>/` carries
- `error_class` plus a truncated error tail.
-
- * **Accuracy eval defaults on.** `_workload_envs.py` now defaults
- `RUN_EVAL=true`; setting `RUN_EVAL=false` is an explicit disable path and
- emits a warning. If a session records `baseline_accuracy=0.0`, the accuracy
- gate still skips because there is no baseline to compare against, so treat
- that as a missing-evidence warning rather than a clean accuracy pass. Before
- relying on multi-node accuracy eval, confirm InferenceX accepts the relevant
- flags via `grep -nR concurrent-requests "$INFERENCEX_PATH/benchmarks"` — if
- the grep is empty, eval may fail every variant including baseline.
-
- * **Image-level launcher-flag denylist (probe each boot,
- framework-aware, model-agnostic)**. Any `backends` grid variant
- whose corresponding launcher CLI flag is not registered on the
- current RayJob image will fail at argparse regardless of model /
- TP. Image rebuilds happen out-of-band; do not carry a hard-coded
- skip list across sessions. On each fresh sandbox boot, after
- `init-env` succeeds, probe the framework launcher from the RayJob
- head pod (the sandbox does not have the inference framework
- installed) and cross-reference its flag set against the grid for
- that framework in
- `inference_optimizer/orchestrator/action_executors/backends.py`:
- * **sglang** — probe via `python3 -m sglang.launch_server --help`;
- grid = `DEFAULT_BACKENDS_GRID` plus the multi-node tier
- additions.
- * **vllm** — probe via `python3 -m vllm.entrypoints.openai.api_server --help`
- (older builds) or `vllm serve --help` (v0.5+); grid =
- `DEFAULT_VLLM_BACKENDS_GRID`.
-
- For each variant in the active framework's grid whose flag the
- probe reports missing, add the variant name to `--skip-variants`
- on `python -m hyperloom.inference_optimizer.cli optimize` (or `SKIP_VARIANTS=...` in the
- prompt). Drop entries the moment a probe shows the flag accepted
- again. The other framework's grid is irrelevant to this run and
- MUST NOT be probed against the wrong launcher.
-
- * **Model-specific variant incompatibilities are NOT a default skip
- list**. A variant that fails on one `(model, TP)` pair is not, by
- itself, evidence that it will fail on another. Do NOT auto-extend a
- known-bad list across model classes by analogy — re-probe per model
- and let the runtime's own rejected-variant ledger
- (`explore_search.rejected` in `state.json`) accumulate evidence
- instead. If you genuinely need to
- skip a variant for the *current* model, capture the failure first
- (let it run once and surface the per-variant abort marker the grid
- runner writes), then state that decision in the prompt with the
- model name spelled out — never propagate the skip silently into the
- next session.
+ * **Exit 2 "no cluster"** → hand-off env missing. Check
+ `HYPERLOOM_MN_EXT_SERVICE_URL` (+ infera `_SSH_KEY` and one `*_IPS`);
+ provisioning is upstream of this CLI.
+ * **`ModuleNotFoundError: sglang`** + stderr shows `sglang.launch_server` on the
+ sandbox → the client-only rule above tripped; fix env propagation.
+ * **`restart-server` succeeded then `/health` timed out** → framework early-exit;
+ the driver exits 2 + `MULTI_NODE_FAILURE_SNAPSHOT={kind:"framework_early_exit"}`
+ and the `rank_0.log` tail names the cause.
+ * **Cross-node NCCL/RCCL collective hangs → watchdog abort, weights never load**
+ (server GPU mem stays near 0) → the pods lack a usable RDMA device
+ (`ibv_devices` empty, no `/dev/infiniband`) while NCCL is forced onto IB
+ (`NCCL_IB_HCA` set). This is a **platform provisioning** issue — the pods must
+ request `rdma/hca` (a partial-GPU multi-node task must still get RDMA). To
+ confirm/unblock, force TCP by exporting
+ `HYPERLOOM_MN_EXTRA_FWD_ENV='{"NCCL_IB_DISABLE":"1"}'` before `optimize`: that
+ is the channel both backends forward to the pods (rayjob via the job
+ `runtime_env`, infera via the SSH fan-out). Slow but works. **Not**
+ `--extra-env` — that reaches the variant filter only and never leaves the
+ sandbox, so using it here looks like a clean result and rules nothing out.
+ * **Variant aborts with no benchmark output** → read `failed_variants` in the
+ round's `<action>_attempts.extras`, or the per-variant `abort_reason.json`
+ under `${USER_DATA_PATH}/runs/<action>/<task>/<variant>/` (`error_class` +
+ error tail).
+ * **Launcher-flag rejection** → a `backends` grid variant whose CLI flag isn't
+ on the current image fails at argparse. Probe the launcher from a GPU pod
+ (`sglang.launch_server --help` / `vllm serve --help`) and `--skip-variants`
+ the missing ones; drop the skip when the probe shows the flag again. Do not
+ carry a hard-coded skip list across sessions or model classes.
## Interpreting a low-gain result
- * A small validated gain with `kernel_opt_outcome="skip"` / `totals.attempted=0`
- (`reports/kernel_optimization_summary.json`) means the profiled step was
- host-bound. Kernel candidates come only from the TraceLens `analysis.md`, and
- `tracelens_analysis._evaluate_high_idle_gate` suppresses them when the step is
- mostly GPU-idle; only structural levers (GPU-graph capture + batching) apply.
- * Under PD-disaggregation + DP-attention the per-rank steady-state batch can be
- bs1 even at high client concurrency, so TraceLens splits into
- `*_steady_state_..._bs1_conc1` windows. If every file in
- `tracelens/trace_split/` is `bs1_conc1`, the trace is host-bound and kernel_opt
- skips by design.
- * Kernel candidates require a compute-bound profile (steady-state bsN, N>1). The
- lever is the per-rank step batch (DP/PD topology), not client concurrency. On
- multi-node this is automatic: a host-bound profile (high GPU idle + 0
- candidates) triggers one compute-bound re-profile with DP-attention/dp-size
- stripped (single DP rank, full per-step batch); candidates found are still
- validated on the served config. Disable with
- `HYPERLOOM_PROFILE_AUTO_COMPUTE_BOUND=0`.
+ * Small validated gain with `kernel_opt_outcome="skip"` /
+ `totals.attempted=0` (`reports/kernel_optimization_summary.json`) → the step
+ was host-bound; TraceLens suppresses kernel candidates on GPU-idle steps, so
+ only structural levers (graph capture + batching) apply.
+ * Under PD + DP-attention the per-rank steady-state batch can be bs1 even at
+ high client concurrency; if every `tracelens/trace_split/` file is
+ `bs1_conc1`, the trace is host-bound and kernel_opt skips by design. Kernel
+ candidates need a compute-bound profile (steady-state bsN, N>1); multi-node
+ auto-re-profiles with DP-attention stripped (disable with
+ `HYPERLOOM_PROFILE_AUTO_COMPUTE_BOUND=0`).
## Disaggregated + DP-attention prerequisites
- * `--enable-dp-attention` / `--enable-dp-lm-head` are no-ops without `--dp-size N`
- (N>1); sglang force-disables them at `dp_size==1`.
- `launch_infera_node._build_sglang_cmd` auto-injects `--dp-size = tp` when a
+ * `--enable-dp-attention` / `--enable-dp-lm-head` are no-ops without
+ `--dp-size N` (N>1); `launch_infera_node` auto-injects `--dp-size = tp` when a
dp-attention flag is present and `--dp-size` is absent (explicit value wins).
- * With `dp_size>1` sglang binds one kv-events ZMQ socket per DP rank at
- `base+rank`; the image must ship `infera.common.net.free_tcp_port_block`. An
- image without it allocates a single port and decode crash-loops with
- `zmq.error.ZMQError: Address already in use` — rebuild the image from current
+ * With `dp_size>1` sglang binds one kv-events ZMQ socket per DP rank; the image
+ must ship `infera.common.net.free_tcp_port_block`, else decode crash-loops
+ with `zmq.error.ZMQError: Address already in use` — rebuild from current
Optimus.
- * KV transfer backend default is `mori`.
## Readiness gate
- `_wait_for_server_health_async` requires, in order: `/health` 200 ->
- `/v1/models` non-empty -> `/v1/completions` (`max_tokens>=2`, `ignore_eos`)
+ `_wait_for_server_health_async` requires, in order: `/health` 200 →
+ `/v1/models` non-empty → `/v1/completions` (`max_tokens>=2`, `ignore_eos`)
returning `completion_tokens>=2`, twice. It fast-fails when `/v1/models` stays
empty for `HYPERLOOM_MN_MODELS_EMPTY_GRACE_S` (default 600s) after `/health` is
up (workers crashed on launch). Tunables:
`HYPERLOOM_MN_COMPLETION_PROBE_{COUNT,TOKENS,MIN_TOKENS}`,
- `HYPERLOOM_MN_MODELS_EMPTY_GRACE_S`.
+ `HYPERLOOM_MN_MODELS_EMPTY_GRACE_S`, `HYPERLOOM_MN_HEALTH_WAIT_S`.