Skip to content

[NV] llm-d: re-test max-tpt in-sweep with unique SLURM job-name fix#2117

Closed
ezrasilvera wants to merge 28 commits into
mainfrom
nv-llm-d-max-tpt-jobname-fix
Closed

[NV] llm-d: re-test max-tpt in-sweep with unique SLURM job-name fix#2117
ezrasilvera wants to merge 28 commits into
mainfrom
nv-llm-d-max-tpt-jobname-fix

Conversation

@ezrasilvera

Copy link
Copy Markdown
Collaborator

What

Isolated re-test of the max-tpt (3P+1D DEP8 conc4096) llm-d-vllm multinode
benchmark with a fix for the in-sweep failure.

Why

max-tpt passes as a standalone e2e (run 28748597199, ~8747 tok/s/GPU) but its
throughput job was killed by an external SIGTERM ~10 min into weight load in
the isolated in-sweep run 28789948711 - before the server came up, so no
result JSON was produced and the launch step failed with "No benchmark result
files found". The engine loaded cleanly with zero errors and the twin eval job
(same server config, separate nodes) passed gsm8k at em_strict 0.959, so this
is not a config or engine problem.

Fix

submit.sh set the SLURM --job-name to $RUNNER_NAME (e.g. gb200-nv_1),
which is stable across runs. The workflow "Slurm cleanup" steps run
scancel --user=$USER --name=<runner.name> at pre-run and post-run, so a
bare runner-name allocation can be cancelled by any concurrent/adjacent job on
the shared cluster+user that shares the runner name. This change appends
GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT to the job name so an exact
--name=<runner.name> scancel from elsewhere no longer matches this
allocation.

The sweep is isolated to the single max-tpt config-key. If max-tpt now passes
in-sweep, the name-scoped scancel was the cause; if it still fails, this rules
it out and points to an environmental/scheduling cause.

Add a new llm-d-vllm disaggregated (prefill/decode) multi-node benchmark
path to InferenceX, orchestrated entirely by SLURM (no Kubernetes). vLLM
engines with llm-d EPP routing, an Envoy front-end, and NIXL KV transfer.

- benchmarks/llm-d/: container image (Dockerfile on vLLM v0.23 base),
  Envoy + EPP config, binaries.env, and an extraction helper that
  decouples epp/sidecar/envoy from the vLLM image.
- benchmarks/multi_node/llm-d/: server.sh orchestrator (per-role TP/EP,
  prefill/decode coordinators, PREFILL_WORKERS multi-engine prefill),
  job.slurm (pyxis/enroot + docker paths, per-node model probe), submit.sh.
- runners/launch_gb200-nv.sh: dispatch branch for FRAMEWORK=llm-d-vllm.
- benchmarks/benchmark_lib.sh: anchor a relative lm_eval task yaml to the
  repo root so evals resolve inside the serving container.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Route dsv4 models through the vLLM tokenizer wrapper and allow
deepseek_v4 as a --tokenizer-mode choice, bypassing HF AutoTokenizer
which does not recognize the model on the benchmark image. Adds
--trust-remote-code / --use-chat-template / --dsv4 passthroughs.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
- benchmarks/multi_node/dsv4_fp4_gb200_llm-d-vllm.sh: GB200 wrapper
  (GPUS_PER_NODE=4) for the DSV4-Pro FP4 llm-d-vllm path.
- llm-d-recipes/dsv4-fp4-gb200-mid-curve-megamoe.yaml: 1P+1D DEP8 recipe.
- nvidia-master.yaml: register the mid-curve-megamoe (conc 256/512/1024)
  and high-tpt-megamoe (Option-B 2P+1D, conc 4096) config keys.
- perf-changelog.yaml: changelog entry for the two keys.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…diagnostics

The decode leader's prefill-readiness curl had no connect/max timeout, so a
blackholed prefill endpoint made a single curl block forever - the 5-min
deadline is only checked between calls, so it never fired. A 2P
(PREFILL_WORKERS=2) run wedged ~7h on this before being cancelled.

- Add --connect-timeout 5 --max-time 10 so an unreachable endpoint trips the
  5-min deadline and FATALs cleanly instead of hanging the whole run.
- Probe every prefill node (ALL_IPS[0:PREFILL_NODES]), not just IPS[0]:
  endpoints.yaml lists one prefill endpoint per node, and PREFILL_WORKERS>1
  spans multiple independent DP engines, so IPS[0] only covered engine 1.
- On failure, dump network diagnostics - source IP toward target,
  /proc/net/route on-link-vs-gateway subnet classification, bash /dev/tcp L4
  connect test, ping, verbose curl - to separate a server-not-ready problem
  from a subnet/routing/firewall problem. Log the subnet layout up front too.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Two teardown fixes so a failed/hung benchmark can no longer wedge the whole
SLURM allocation (a conc4096 2P run hung 8h until GitHub killed it at the
max-execution limit, losing the server-log tarball):

- server.sh coordinator: write the .bench_done release marker from an EXIT
  trap, so job.slurm scancels the allocation on ANY coordinator exit (failed
  bench, set -e abort, EPP/Envoy bring-up error), not only a clean finish.
- benchmark_lib.sh: optional `timeout` wrapper around the bench client, gated
  on BENCH_TIMEOUT_S (default 2400s, set by server.sh); a hung client (asyncio
  shutdown wedged on thousands of failed in-flight requests) is killed instead
  of pinning 24 GPUs to TIME_LIMIT. The bench call is also made non-fatal so a
  multi-conc sweep records every point it can and always reaches teardown.

Replace the leader-only .prom scraper with metrics_scrape.py, which scrapes
EVERY prefill and decode vLLM (on the vLLM port, not the sidecar) plus EPP into
one time-correlated CSV. This makes per-engine load visible - crucially whether
EPP fans out across all prefill engines - which the old PREFILL_LEADER_IP-only
scraper could not show for PREFILL_WORKERS>1.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
dsv4-fp4-gb200-llm-d-vllm-smoke-2p1d: two independent 1-node DEP4 prefill
engines (PREFILL_NODES=2 PREFILL_WORKERS=2) + 1 DEP4 decode = 3 nodes / 12 GPUs,
ISL/OSL 1024/128, single conc 128 x ~512 prompts, RANDOM_RANGE_RATIO=1.0. Runs
in minutes; validates the per-engine metrics scraper and confirms EPP fans out
across both prefill engines before spending the 6-node high-tpt run. Reuses the
mid-curve recipe.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
The 3-node DEP4 smoke (1 node/engine) OOMs at model load: DSV4-Pro's MegaMoE
needs each EP rank to hold 1/4 of the experts at EP4 (~2x the per-GPU weight of
the validated EP8), which does not fit in HBM (run 28588980717:
"CUDA Error: out of memory at cumem_allocator.cpp:139"). DEP8 (2 nodes/engine)
is the minimum viable EP. Smoke is now PREFILL_NODES=4 PREFILL_WORKERS=2 (2x
DEP8) + DECODE_NODES=2 = 6 nodes, same layout as high-tpt but a tiny workload
(conc 128), so it validates the scraper + EPP fan-out fast on the real topology.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
LLMD_LB_MODE (recipe env, default hybrid). multi-port switches both roles to
--data-parallel-multi-port-external-lb + --data-parallel-supervisor-port and
drops --api-server-count, so each DP rank serves on VLLM_PORT+rank (base+global
rank; a worker node binds VLLM_PORT+START_RANK..). The decode pd-sidecar gets
--data-parallel-size=$DP_SIZE, which per data_parallel.go makes it expose
SIDECAR_PORT+i -> vLLM VLLM_PORT+i; per node only its local rank ports work, so
endpoints.yaml lists each node's local rank ports (node j: base + j*GPUS_PER_NODE
.. +GPUS_PER_NODE). Health waits + the metrics scraper follow the same per-rank
ports (scraper hits the vLLM port, never the sidecar). Mirrors the upstream
GENERIC wide-ep-lws guide (DeepSeek-R1), which uses per-rank ports for P/D; our
DSV4 path was hybrid-lb (one port/node).

Recipe dsv4-fp4-gb200-megamoe-mp.yaml = mid-curve engine config + LLMD_LB_MODE
multi-port + EPP scorers matched to the upstream router config (drop
kv-cache-utilization-scorer; prefill = prefix-cache/queue/active-request,
decode = active-request only). Keys smoke-2p1d-mp (fast validation) and
high-tpt-megamoe-mp (conc4096 A/B vs the hybrid high-tpt key) select it. Hybrid
path is unchanged (default), so existing keys are byte-identical.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
mp-smoke 28596473295 showed every prefill node (leader AND worker) binds
VLLM_PORT+local_index (8200-8203), not base+global rank: --data-parallel-start-rank
only sets DP rank IDs, the HTTP servers are always the node-local range. My
base+global assumption made the health gate probe a dead worker port
(10.30.1.39:8204 not ready). Fix everywhere to base+local:
  - HEALTH_PORT / SIDECAR_HEALTH_PORT = VLLM_PORT / SIDECAR_PORT (local rank-0).
  - sidecar --data-parallel-size = DP_SIZE_LOCAL (each node exposes its 4 local
    ranks at SIDECAR_PORT..+3 -> vLLM VLLM_PORT..+3).
  - endpoints + scraper: each node lists base..base+GPUS_PER_NODE-1 (same range
    on every node, distinct address).
Also fix a double 'export export LLMD_LB_MODE' from overlapping edits.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
… bundle

At conc4096 both Dynamo (8153 tok/s/GPU) and upstream llm-d PR #1737 (8858)
succeed on the IDENTICAL 2P TP1/DP8/EP8 layout we use, while ours collapses to
8.3% completion. The difference is the engine bundle, so this recipe applies it:
prefill gpu-mem 0.95, max-model-len 9280, max-num-seqs 16, max-num-batched-tokens
32768, no-enable-flashinfer-autotune; decode gpu-mem 0.9, max-model-len 9280,
no-flashinfer-autotune. Same EPP/env/topology + kv producer-consumer + hybrid-lb
as mid-curve, to isolate the engine bundle. The max-model-len/no-autotune/
max-num-seqs guards are what let this 32768/0.95 bundle bring up without the two
OOMs our earlier speed-bundle hit. Keys tuned-smoke + tuned-high-tpt select it.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
The no-kube Envoy front door (benchmarks/llm-d/envoy.yaml) defined the epp
ext_proc cluster and the original_dst cluster with no circuit_breakers block,
so Envoy applied its DEFAULT per-cluster thresholds of max_requests=1024 and
max_pending_requests=1024. The ext_proc filter holds one long-lived HTTP/2
stream to EPP for the entire lifetime of every request, so at max-concurrency
4096 only 1024 active + 1024 pending = 2048 requests ever reached EPP; the
remaining ~18432 (warmups + prompts) got a fast 500 and never saw the engine.

This capped every high-tpt run at exactly 1024 completions / 2048 disagg
decisions, identically across engine configs (mid-curve, Dynamo-tuned) and LB
modes (hybrid, multi-port) - because the cap was the front door, not the
engine. It is why Dynamo (no Envoy) and upstream llm-d (k8s gateway) hit
8000+ tok/s/GPU on the same TP1/DP8/EP8 2P1D layout where we stalled at ~1783.

Fix: add circuit_breakers with thresholds far above max-concurrency to both
clusters. Evidence: envoy_access RESPONSE_CODE histogram = 2048x200 + 18432x500
in all three walled runs; disagg_decision_total pinned at 2048 in all three.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…0000

Align the circuit_breakers thresholds added in the previous commit to the exact
values from the authoritative upstream preset - the llm-d-router-standalone Helm
chart (llm-d-inference-scheduler config/charts/llm-d-router-standalone/values.yaml
lines 191-208): max_connections/max_pending_requests/max_requests = 40000 on both
the ext_proc (EPP) and original_dst clusters, plus max_retries 1024 on ext_proc.
Both clear max-concurrency 4096 with wide headroom; 40000 just matches upstream
rather than an arbitrary ceiling.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
… upstream)

The prior CB commit lifted the epp ext_proc cluster to 40000 but left the
original_dst cluster at 1000000. Both clear max-concurrency 4096 identically
(this is the exact config the 8452 tok/s/GPU high-tpt confirmation run used),
but pin original_dst to 40000 too so both clusters match each other and the
upstream llm-d-router-standalone preset.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…util + decode prefix-cache)

Align the mid-curve/high-tpt recipe's EPP scheduling profiles to the upstream
generic wide-ep-lws router config
(llm-d guides/wide-ep-lws/router/wide-ep-lws.values.yaml):
  prefill = prefix-cache(3) + queue(2) + active-request(2)
  decode  = active-request only

Removed: kv-cache-utilization-scorer from prefill (upstream does not use it; its
weight becomes active-request(2)); and prefix-cache-scorer from decode - at
weight 3 it outweighed active-request(2) and pinned decode to one pod on a
shared prompt prefix, producing the ~2:1 decode routing imbalance (decode-0 1389
vs decode-1 659). Upstream decode routes on active-request alone and balances.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…keys

Pre-PR cleanup of debug/experiment scaffolding added while root-causing the
conc4096 wall (now fixed via the Envoy circuit-breaker change). Removed:
- benchmarks/multi_node/llm-d-recipes/dsv4-fp4-gb200-megamoe-mp.yaml (multi-port
  LB experiment - dead: NIXL KV-expiry)
- benchmarks/multi_node/llm-d-recipes/dsv4-fp4-gb200-megamoe-tuned.yaml (Dynamo
  engine-bundle A/B - superseded; the circuit-breaker fix, not engine args, was
  the wall)
- nvidia-master.yaml keys: smoke-2p1d, smoke-2p1d-mp, high-tpt-megamoe-mp,
  tuned-smoke, tuned-high-tpt (experiment/smoke keys tied to the above)

Kept: mid-curve-megamoe + high-tpt-megamoe keys, the envoy circuit_breakers fix,
the teardown/timeout fix, and the DSV4 tokenizer plumbing. Deferred to a
follow-up (after the mid-curve rerun validates and its scraper CSV is analysed):
removing the per-engine vLLM metrics scraper and the multi-port branches in
server.sh.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…pper

- Remove metrics_scrape.py + its scraper bring-up/stop/conc-tag in server.sh
- Remove dead multi-port LB path (LLMD_LB_MODE and all branches); hybrid only
- Remove EPP --v=4 and NCCL/UCX transport debug logging (keep UCX_TLS pin)
- Rename dsv4_fp4_gb200_llm-d-vllm.sh -> ...-disagg.sh; update launcher SCRIPT_NAME

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Comment-only cleanup across the llm-d-vllm GB200 assets:
- recipe header rewritten to describe the common per-engine shape and the
  two topologies (mid-curve 1P1D / high-tpt 2P1D) it backs
- drop historical tuning-log comments from the recipe, launcher, envoy.yaml,
  binaries.env, and Dockerfile
Verified: no non-comment lines changed; envoy circuit_breakers (40000) intact.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Include dsv4-fp4-gb200-llm-d-vllm-high-tpt-megamoe (2P1D DEP8, conc 4096)
alongside the mid-curve key so a labeled PR sweep validates both topologies,
not just mid-curve.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
- llm-d/README.md: document endpoints.yaml as one entry per node (not per
  leader) to match server.sh add_role(); fix example ports (prefill 8200,
  decode 8000) and rewrite the rationale section (C2)
- perf-changelog.yaml: canonicalize pr-link XXX -> 2050 (C4)

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…ent (C1)

Bump the EPP and P/D routing-sidecar source images from the moving
-dev:main tags to release :v0.9.0 in both binaries.env and the
combined-image Dockerfile, so a rebuild is reproducible. Verified both
tags ship linux/arm64 and still expose /app/epp and /app/pd-sidecar
(statically-linked aarch64). Envoy pin unchanged.

Also reword the binaries.env header (PR #2050 review C1): the old
comment claimed the file is 'not used', but job.slurm still sources it
and conditionally bind-mounts the binaries via LLMD_BIN_DIR. Clarify
it is an optional mounted-binary override, a no-op by default, and fix
the 'build an use' -> 'build and use' typo.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…ild-arg

BuildKit rejects `COPY --from=${VAR}` with 'variable expansion is not
supported for --from'. Promote VLLM_BASE and the three source-image ARGs
(EPP/sidecar/envoy) to global scope, bind each to a named FROM stage
(epp_src / sidecar_src / envoy_src), and COPY --from those stage aliases.
No change to defaults or --build-arg override behavior; still in sync
with binaries.env.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
…sses

The eval-only jobs failed 'eval metadata concurrency does not match
workflow request' even though gsm8k accuracy passed (0.95 >= 0.91). Cause:
run_eval/append_lm_eval_summary read EVAL_CONCURRENT_REQUESTS and CONC
(benchmark_lib.sh), never EVAL_CONC, so with CONC empty meta_env.json
recorded conc=1 while validate_scores.py --expected-concs checked 4096.

Mirror the AMD multi-node servers: before run_eval, set
EVAL_CONCURRENT_REQUESTS from EVAL_CONC (fallback: max of the x-delimited
BENCH_MAX_CONCURRENCY) and export CONC so the stamped concurrency matches
the workflow request.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Add dsv4-fp4-gb200-llm-d-vllm-max-tpt-megamoe: 3 prefill (DEP8) + 1 decode
(DEP8) at conc=4096, mirroring NVIDIA/srt-slurm disagg-gb200-max-tpt-megamoe
(engine-identical to high-tpt; only prefill scale-out 2->3), so it reuses the
same CONFIG_FILE. Validated standalone via e2e run 28711505502 at 8797
tok/s/GPU (median TTFT 58 ms) - above high-tpt's 8241 tok/s/GPU, since the
extra prefill instance relieves the conc=4096 prefill bottleneck.

Adds a matching perf-changelog entry under PR #2050.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Move the max-tpt config-key to the top of the changelog entry so it
dispatches first in the serialized multi-node matrix. The prior sweep
(28714892207) had max-tpt land last; a transient decode-node slowdown
degraded that point (4.5x TPOT vs high-tpt on an identical decode
config). Running it first surfaces its result early and re-tests the
transient before the shorter mid/high legs finish.

Ordering only - all three points still run and land on the graph.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
max-tpt (3P1D conc4096) succeeds standalone (8747 tok/s) but dies in-sweep:
the decode's cross-node gloo TCP coordination hits 'Connection reset by
peer' during the conc4096 benchmark warmup burst -> EngineDeadError cascade.
No OOM / CUDA / hardware fault in any rank - it is a transient distributed
coordination reset under load. Raise the torch-distributed process-group
timeout (1800s, matching srt-slurm) on both roles so a momentarily-stalled
peer is not dropped mid-burst. Applies to mid/high/max (shared CONFIG_FILE).

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Triggers the sweep to validate the TORCH_DISTRIBUTED_DEFAULT_TIMEOUT=1800
fix for max-tpt in-sweep (real curve run -> unofficial graph), rather than
a throwaway e2e.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
Max-tpt (3P+1D DEP8 conc4096) passes as standalone e2e but fails
when run alongside mid-curve + high-tpt in the same sweep matrix.
Drop mid-curve and high-tpt from the sweep entry temporarily to
confirm whether the failure is sweep-harness-specific.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>
… scancel

The max-tpt (3P+1D DEP8 conc4096) benchmark passes as a standalone e2e but
its throughput job was killed by an external SIGTERM ~10 min into weight
load in the isolated in-sweep run 28789948711, before the server came up.
No result JSON was produced, so the launch step failed with "No benchmark
result files found". The engine logs show a clean load with zero errors,
and the twin eval job (same server config, separate nodes) passed gsm8k at
em_strict 0.959 - so this is not a config or engine problem.

submit.sh set the SLURM --job-name to "$RUNNER_NAME" (e.g. "gb200-nv_1"),
which is stable across runs. The workflow "Slurm cleanup" steps run
"scancel --user=$USER --name=<runner.name>" at pre-run and post-run, so a
bare runner-name allocation can be cancelled by any concurrent or adjacent
job on the shared cluster+user that shares the runner name - not only its
own cleanup.

Fix: include GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT in the SLURM job name so
an exact "--name=<runner.name>" scancel from elsewhere no longer matches
this allocation. The runner-name prefix is kept first so per-runner
cleanup and greps still recognize the job.

Also isolates the sweep to the single max-tpt config-key to re-test the
fix in isolation. If max-tpt now passes in-sweep, the name-scoped scancel
was the cause; if it still fails, this rules it out.

Signed-off-by: Ezra Silvera <ezra@il.ibm.com>

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 configs/nvidia-master.yaml:9075-9078 — The merge-blocker comment at lines 9075-9078 says 'the image: on both llm-d-vllm keys below (mid-curve + high-tpt) is a personal build ... Republish ... before merge', but this PR adds a THIRD sibling key dsv4-fp4-gb200-llm-d-vllm-max-tpt-megamoe (line ~9160) that also uses ghcr.io/ezrasilvera/llm-d-nokube-vllm:vllm0.23 — and it is the config this PR is actively benchmarking (per the perf-changelog entry). Updating the enumeration to "all three (mid-curve + high-tpt + max-tpt)" avoids a comment-driven cleanup leaving max-tpt on the personal image at merge time.

    Extended reasoning...

    What's stale. The block introduced in the earlier llm-d PR added a merge-blocker note above the mid-curve/high-tpt entries. Verbatim: "PR follow-up (merge-blocking): the image: on both llm-d-vllm keys below (mid-curve + high-tpt) is a personal build. Republish it to the org registry ... and update these image: lines before merge." This PR then adds a third sibling — dsv4-fp4-gb200-llm-d-vllm-max-tpt-megamoe — that reuses the same ghcr.io/ezrasilvera/llm-d-nokube-vllm:vllm0.23 image, so the enumeration "both ... (mid-curve + high-tpt)" is now numerically wrong (three, not two).\n\nWhy it matters. The comment is explicitly a merge-blocking TODO whose intent is republish and swap all sibling image: lines. A reader executing that TODO by following the enumeration literally — the way in-file comments are normally read — could swap the two named keys and leave max-tpt-megamoe pointing at the personal registry. Given that this PR is specifically the max-tpt re-test (per its title and the perf-changelog entry it adds), leaving that one entry on the personal image would be a real regression for the very config this PR ships.\n\nStep-by-step proof.\n1. Reviewer (or the PR author on the last pass) reads nvidia-master.yaml:9075-9078, sees "the image: on both llm-d-vllm keys below (mid-curve + high-tpt) is a personal build".\n2. They locate the two enumerated keys: dsv4-fp4-gb200-llm-d-vllm-mid-curve-megamoe (line 9079) and dsv4-fp4-gb200-llm-d-vllm-high-tpt-megamoe (line ~9115), swap both image: lines to the republished org URL.\n3. They stop, because the comment said "both". The third key dsv4-fp4-gb200-llm-d-vllm-max-tpt-megamoe at line ~9160 remains on ghcr.io/ezrasilvera/llm-d-nokube-vllm:vllm0.23.\n4. Sweep runs against the personal image for the config this PR advertises as validated.\n\nAddressing the refutation. The refutation argues (a) all three image: lines are visible immediately below the comment, (b) grep ezrasilvera catches all three, and (c) perf-changelog announces max-tpt-megamoe. All true, but each depends on the cleanup being done via a search rather than by following the comment's enumeration verbatim. The whole reason this comment exists is that a maintainer will read it before merge — and that reader is being told there are two entries when there are three. Fix cost is one line; the alternative is trusting every future reader to spot the mismatch on their own.\n\nFix. Either (preferred) republish and swap all three image: lines before merge and drop the comment, or update the enumeration to "the three llm-d-vllm keys below (mid-curve + high-tpt + max-tpt)" so a comment-driven cleanup covers max-tpt. Severity is "nit" — this is a comment consistency issue, not a bug in shipped behavior; the underlying personal-image concern is already flagged by the same comment.

  • 🟡 perf-changelog.yaml:4560-4566 — The pr-link for the new dsv4-fp4-gb200-llm-d-vllm-max-tpt-megamoe entry (perf-changelog.yaml:4566) points to /pull/2050, but this PR is #2117 and the description block (updated in HEAD commit 10aeffc) describes THIS PR's specific SLURM --job-name fix. If #2050 is being intentionally used as the canonical parent for all llm-d entries, feel free to dismiss; otherwise consider updating to /pull/2117 to match the convention used by sibling entries (#1994, #2010, #2077, etc.).

    Extended reasoning...

    What / how it manifests

    perf-changelog.yaml:4566 sets pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2050 for the dsv4-fp4-gb200-llm-d-vllm-max-tpt-megamoe entry. Per the PR metadata, this is PR #2117 ([NV] llm-d: re-test max-tpt in-sweep with unique SLURM job-name fix).

    Why the description text points at #2117, not #2050

    The description block reads:

    • "Isolated max-tpt (3P+1D DEP8 conc4096) re-test with unique SLURM job-name fix"
    • "max-tpt passes standalone e2e but was killed mid weight-load in sweep (run 28789948711)"
    • "Fix: SLURM --job-name now includes GITHUB_RUN_ID/ATTEMPT so a name-scoped scancel from another job on the shared cluster cannot cancel this allocation"

    Every one of these three bullets describes the SLURM --job-name fix that lives in this PR's HEAD commit (10aeffc [NV] llm-d: unique SLURM job-name to protect max-tpt from name-scoped scancel), which touches benchmarks/multi_node/llm-d/submit.sh to append GITHUB_RUN_ID / GITHUB_RUN_ATTEMPT. The referenced run 28789948711 is the failed sweep noted in PR #2117's description. None of this content lives under PR #2050.

    Why the file convention says #2117

    The other recent entries in perf-changelog.yaml all use their own PR number as pr-link (e.g. #1994 for the minimaxm3-fp4-b300 high-conc add, #2006 for the mtp update, #2007 for the B200 eagle update, #2010 for the DSV4 GB300 dynamo update). The natural reading is that pr-link points at the PR that lands the entry — under that reading, this entry should read /pull/2117.

    Step-by-step

    1. PR #2117 is open with head 10aeffc. git log on this branch shows it and its recent commits.
    2. That head commit modifies the exact perf-changelog block at lines 4560-4566 to describe the SLURM job-name fix.
    3. The submit.sh diff in this same PR is exactly that fix: SLURM_JOB_NAME="${RUNNER_NAME}-${GITHUB_RUN_ID:-manual}-${GITHUB_RUN_ATTEMPT:-0}", replacing the prior bare-$RUNNER_NAME job name — which is what the description bullets attribute to "this PR".
    4. So the description text is unambiguously about #2117, while pr-link says #2050.

    Addressing the counter-argument

    A refuting verifier noted that an earlier branch commit (06f8b3c [NV] llm-d: address PR #2050 review nits (docs + changelog pr-link)) explicitly canonicalized pr-link: XXX -> 2050 for the ORIGINAL llm-d entry (lines 4538-4541, about mid-curve/high-tpt), and that commit e149e9a (which added the max-tpt entry) says "Adds a matching perf-changelog entry under PR #2050". If the author's intent is that #2050 stays the canonical parent for every llm-d changelog entry — even ones landed by descendant PRs — this is by design, not a bug. That is a legitimate reading and is why this is filed as a nit, not a normal issue.

    The countervailing signal is that HEAD commit 10aeffc (in THIS PR, not #2050) rewrote the description text to describe THIS PR's fix specifically, which is inconsistent with "the entry belongs to #2050": if #2050 were truly canonical, the description would describe #2050's content, not #2117's.

    Impact

    Documentation-only. No functional or CI impact. Audit trails from the changelog to PR context would follow the wrong link (readers land on #2050 expecting to see the SLURM job-name fix, and find the original llm-d landing PR instead).

    Fix

    If unintentional: change line 4566 to pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2117.
    If intentional (#2050 as canonical parent): dismiss this comment; consider phrasing the description in past-tense parent-PR terms rather than "this PR" terms to avoid the mismatch.

Comment on lines +210 to +218

PYXIS_ENV_LIST="NUM_NODES,PREFILL_NODES,DECODE_NODES,ALL_IPS,PREFILL_LEADER_IP,DECODE_LEADER_IP"
PYXIS_ENV_LIST+=",PREFILL_WORKERS,DECODE_WORKERS"
PYXIS_ENV_LIST+=",PREFILL_DP_ADDR,DECODE_DP_ADDR,MODEL_NAME,GPUS_PER_NODE"
PYXIS_ENV_LIST+=",PREFILL_DP_SIZE,DECODE_DP_SIZE"
PYXIS_ENV_LIST+=",BENCH_INPUT_LEN,BENCH_OUTPUT_LEN,BENCH_MAX_CONCURRENCY"
PYXIS_ENV_LIST+=",BENCH_REQUEST_RATE,BENCH_RANDOM_RANGE_RATIO,BENCH_NUM_PROMPTS_MULTIPLIER"
PYXIS_ENV_LIST+=",RUN_EVAL,EVAL_ONLY,EVAL_CONC,FRAMEWORK,PRECISION,MODEL_PREFIX"
PYXIS_ENV_LIST+=",RUNNER_TYPE,RESULT_FILENAME,SPEC_DECODING,IS_MULTINODE,CONFIG_FILE"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The pyxis branch of job.slurm builds PYXIS_ENV_LIST (lines 211-218) as an explicit --container-env whitelist but omits MAX_FAILURE_RATE, while the docker branch at line 167 forwards it via -e MAX_FAILURE_RATE=${MAX_FAILURE_RATE:-0.05}. Since launch_gb200-nv.sh sets LLMD_CONTAINER_ENGINE=pyxis unconditionally for the GB200 llm-d-vllm path, the recipe's MAX_FAILURE_RATE=0.5 never reaches the container and benchmark_serving.py:971 falls back to the hardcoded 0.05 default. Fix: append ,MAX_FAILURE_RATE to PYXIS_ENV_LIST (symmetric with the docker -e line).

Extended reasoning...

What / where. The pyxis srun in benchmarks/multi_node/llm-d/job.slurm uses --container-env="$PYXIS_ENV_LIST" to whitelist which env vars propagate into the container. PYXIS_ENV_LIST is built at lines 211-218 and enumerates 30 variables — but MAX_FAILURE_RATE is not one of them. The docker branch at line 167 explicitly forwards it via -e MAX_FAILURE_RATE=${MAX_FAILURE_RATE:-0.05}, and the surrounding PYXIS_ENV_LIST mirrors the docker -e list otherwise. The author's own comment at lines 197-198 ("Pre-export non-SLURM static vars so pyxis --container-env= whitelisting can pick them up") confirms the whitelist design intent.

Step-by-step proof.

  1. benchmarks/multi_node/llm-d/submit.sh:88 runs export MAX_FAILURE_RATE=${MAX_FAILURE_RATE:-0.05} — value reaches sbatch env.
  2. All three new GB200 llm-d-vllm recipes set MAX_FAILURE_RATE=0.5 in additional-settings (configs/nvidia-master.yaml:9106, :9152, :9196).
  3. runners/launch_gb200-nv.sh sets LLMD_CONTAINER_ENGINE=pyxis unconditionally — the docker branch is dead code on this SKU.
  4. job.slurm translates the sbatch env through --container-env="$PYXIS_ENV_LIST"; since MAX_FAILURE_RATE is not in the list, it is dropped at the container boundary.
  5. Inside the container utils/bench_serving/benchmark_serving.py:970-972 does os.environ.get("MAX_FAILURE_RATE", "0.05") — it silently gets 0.05.
  6. At line 975, failure_rate > 0.05 triggers raise SystemExit("FAIL: request failure rate ...") and no result JSON is written.

Impact on this PR. The concrete impact is contingent on the actual failure rate exceeding 5% at conc=4096 with 3P+1D DEP8. The standalone e2e run (28748597199, ~8747 tok/s/GPU) referenced in the PR description already passed on the same pyxis path with the same missing forwarding — strong empirical evidence that either pyxis is propagating the variable via default env inheritance on this cluster, or that the natural failure rate is well under 5% and the 0.05 default is sufficient. Either way, this in-sweep re-test is likely to reflect the same behavior. The current isolated run's observed failure was a SIGTERM during weight load (before any requests were served), which is orthogonal to MAX_FAILURE_RATE.

Why still worth flagging. The docker/pyxis asymmetry is a real contract inconsistency — every other variable the docker branch -es is explicitly enumerated in PYXIS_ENV_LIST. The recipes explicitly opted into a 0.5 tolerance, and if a future transient 5xx spike (well within that tolerance) ever pushes the failure rate above 5%, the run would fail with a symptom (FAIL: request failure rate) that could be confused with the SIGTERM cause this PR is trying to isolate.

Fix. One-line append to PYXIS_ENV_LIST at line 218 or similar:

PYXIS_ENV_LIST+=",MAX_FAILURE_RATE"

Symmetric with the docker -e MAX_FAILURE_RATE=${MAX_FAILURE_RATE:-0.05} line at 167. Also worth adding MAX_FAILURE_RATE to the pre-export block at lines 208-209 for symmetry, though it's already exported by submit.sh.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@ezrasilvera

Copy link
Copy Markdown
Collaborator Author

Diagnostic complete. The isolated max-tpt re-test with the unique SLURM job-name fix passed (run 28918650203, 8730 tok/s/GPU), confirming the in-sweep failure was a name-scoped scancel, not the config. Folding the fix and restoring the full curve (mid-curve + high-tpt) into llm-d-initial (PR #2050); closing this diagnostic PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

1 participant