Skip to content

Kimik3 madengine integration - #242

Open
Cemberk wants to merge 41 commits into
ROCm:developfrom
Cemberk:kimik3-madengine-integration
Open

Cemberk wants to merge 41 commits into
ROCm:developfrom
Cemberk:kimik3-madengine-integration

Conversation

@Cemberk

@Cemberk Cemberk commented Sep 1, 2026

Copy link
Copy Markdown

built on top of existing kimi k3 pr for madengine enablement and testing

Cemberk and others added 13 commits August 13, 2026 17:17
… selector

The xPyD harness already models pools that span multiple nodes (xP/yD count
NODES, roles are MASTER/CHILD via --data-parallel-start-rank + --headless), but
the wideEP path hardcoded TP=1: dp_size was xP*GPUS_PER_NODE and the connector
emitted a literal `-tp 1`. That makes any model whose REPLICATED (non-expert)
weights exceed one GPU inexpressible - e.g. Kimi-K3 on MI300X, where TP1/DP16
needs 190.7 GiB/GPU (> 192 GB HBM) and TP2 is required to fit.

- vllm_disagg.sh: topology math is TP-aware. dp_per_node = GPUS_PER_NODE/TP_SIZE
  feeds dp_size, dp_size_local and both start-rank calculations. TP_SIZE defaults
  to 1 and is validated to divide GPUS_PER_NODE.
- moriio.sh: emit -tp ${TP_SIZE}; clamp --api-server-count to dp_size (it must be
  <= data-parallel-size or the frontend DP balancer routes to ranks that do not
  exist - only reachable once TP>1 shrinks dp_size below GPUS_PER_NODE).
- moriio.sh: advertise the peer pool's node IPs as moriio_pod_hosts when a pool
  spans >1 node. Without it the connector falls back to the peer MASTER only and
  KV writes aimed at ranks on a peer CHILD node silently miss, so those ranks
  decode with no context. Emitted only when xP>1 || yD>1.
- run_xPyD_models.slurm: forward TP_SIZE/GPUS_PER_NODE, wire BENCHMARK_SCRIPT=niah
  to the already-present benchmark_niah.sh, forward NIAH_* knobs.

Default-neutral: at xP=1 yD=1 TP_SIZE=1 - the topology every registered entry
uses - DRY_RUN output is byte-identical to develop across all four node roles.
At xP=2 yD=2 TP_SIZE=2 it emits tp=2, dp_size=8, dp_local=4, child start-rank 4
(= EP16 per pool over 16 GPUs).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Kimi-K3 recipe

Two parts.

1) moriio.sh wideEP dropped ${model_args[@]} on the floor. models.yaml documents
   per-model dp: tuning as supported ("both connectors now append it") and rixl's
   deepep path does append it, but the moriio wideEP branch parsed the flags,
   logged them, and then emitted an argv without them. Verified with a sentinel
   flag: present in the log line, absent from the command. Now appended, matching
   rixl. Every existing wideEP model has an empty dp: block, so emitted argv is
   unchanged for all of them.

2) Kimi-K3 recipe, entirely as data: env: block for the gfx942 knobs
   (VLLM_ROCM_USE_AITER_MLA=0 - the AITER MLA kernel is gfx950-only -
   AITER_SITUV2_A8W4=1 for the packed-int4 SiTUv2 MoE path, KDA conv state layout,
   40e9 KV cache for >600K contexts, per-role cudagraph + MoRI backends), and a
   dp: block for the serve flags the connector does not emit (reasoning parser,
   1M max-model-len, batched-tokens pinned at 2048, MoE quantization-config).
   Registered in MORI_EP_VALID_MODELS and WIDE_EP_ONLY_MODELS.

   The JSON quantization-config carries its own shell quotes: the launcher
   word-splits dp: via eval, which would otherwise strip the JSON's double quotes
   and hand vLLM invalid JSON. Verified the emitted value parses as JSON.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile
  The shared vllm_disagg_inference stack with the pins K3 needs on gfx942
  (MoRI 1.2.2, AITER 0.1.19 + flydsl 0.2.4, the K3+MoRIIO vLLM commit) plus the
  K3-aware AITER graft from the public vendor image - without that graft the K3
  MoE profiling shape finds no tuned FlyDSL config, falls back to a heuristic
  kernel and aborts LLVM inside determine_available_memory.

  Differences from the recipe this is ported from:
  - every source pinned to an immutable SHA, not a fork branch name (branches on
    personal forks can be force-pushed; MAD needs the image rebuildable later)
  - GH_TOKEN build-arg dropped: all three repos are public, and a token passed
    this way is recorded in image metadata
  - the flydsl 0.2.4 re-pin happens at build time, so the launcher no longer runs
    pip install inside every container at serve time
  - WITH_NIXL defaults to 0 (K3 uses the moriio connector only)
  - no runtime patchers: all connector/KDA fixes are committed in the pinned vLLM

scripts/vllm_multinode/
  Colocated (single-instance) multi-node serving - the counterpart to
  vllm_dissag, for models that do not fit one node but want lowest single-request
  latency rather than disagg throughput. TP within a node, PP across nodes.

  It owns only node discovery, the container launch and the head/worker split;
  models.yaml, socket_barrier.py, benchmark_xPyD.sh, benchmark_niah.sh and
  parse_to_csv.py are reused from vllm_dissag (scripts/ is mounted whole), so the
  model recipe and the CSV pipeline have one home.

  The three K3 colocated variants collapse to this one script: dry-run confirms
  pp2xtp8 / allgather / moriep argv differ only by --enable-expert-parallel and
  --all2all-backend, so they become three models.json entries rather than three
  near-identical run.sh copies.

Not built here - this environment has no docker and one 8-GPU node; the harness
is verified by DRY_RUN argv inspection only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
models.json: four entries, all skip_gpu_arch gfx950 (the existing four Kimi-K3
entries are gfx950/TP8 single-node and all carry skip_gpu_arch gfx942, so K3 is
currently skipped entirely on MI300X - this is that gap):

  pyt_vllm_disagg_mori_kimi-k3               -N 4  xP2 yD2 TP2 -> EP16/pool
  pyt_vllm_kimi-k3_mi300x_pp2xtp8            -N 2  TP8 x PP2, no EP
  pyt_vllm_kimi-k3_mi300x_wideep_allgather   -N 2  + EP, allgather_reducescatter
  pyt_vllm_kimi-k3_mi300x_wideep_moriep      -N 2  + EP, mori_low_latency

The three colocated entries differ only in ENABLE_EP / ALL2ALL_BACKEND /
AITER_SITUV2_A8W4, which is why they share one launcher.

Result reporting: BENCHMARK_SCRIPT=niah previously produced logs and nothing
else, so a model declaring multiple_results would have reported no metric.
- parse_to_csv.py gains --niah: parses benchmark_niah.py output and writes the
  same 29-column madengine perf.csv as the throughput path (verified identical
  column sets). One row per context size, performance = needles found /10. A
  size that errored is written as a FAILURE row with performance 0 rather than
  dropped, so a pass->crash regression is visible instead of silent.
- benchmark_niah.sh calls it.
- Both slurm launchers copy /run_logs/$SLURM_JOB_ID/perf.csv to
  ./perf_$MODEL_NAME.csv on completion: madengine resolves multiple_results
  against the job directory it launched from, not the container log mount.
  No-op for entries that do not declare multiple_results.

Verified: madengine discover goes 133 -> 137 with exactly these four added and
none removed; every referenced dockerfile and script exists; -N matches each
topology; the int4 quantization-config survives shell word-splitting as valid
JSON; DeepSeek moriio and rixl/deepep DRY_RUN output is byte-identical to
develop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the writeups from PR ROCm#193 next to the existing gfx950 K3 doc, keyed to the
madengine tags rather than to shell scripts: topology rationale (why MI300X needs
multi-node and why the disagg pool needs TP2), the gfx942 knobs and where each
one lives now, the NIAH results, and the three root-cause fixes.

Corrections carried in:
- the NIAH ceiling is 900K throughout. The upstream READMEs still said 300K in
  seven places after two later commits raised it to 500K and then 900K.
- states that the NIAH harness sizes context in WORDS (~1.3x tokens) while the
  results table is in tokens, so the two columns are not comparable.

benchmark/kimi_k3/README.md gains a pointer: all four entries there carry
skip_gpu_arch gfx942, which is exactly the gap the MI300X page fills.

Every tag, node count and knob value in the new page is checked against
models.json and models.yaml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…metadata

The four MI300X Kimi-K3 entries could not run correctly on a cluster. Three
separate defects, all from the colocated launcher inheriting conventions that
only hold for the disaggregated one.

Node sizing. The entries carried `"args": "-N 4 -n 4"` on the assumption that
madengine forwards args to sbatch. It does not — args are appended to
`bash <model>.slurm`, and neither launcher parses $@, so the flags were dropped
and the job was submitted with the `slurm.nodes` default of 1. Sizing now uses
`distributed.nnodes`, which madengine reconciles into `#SBATCH --nodes`, and the
inert args are removed. `slurm.nodes` is deliberately left unset: setting it
selects the multi-node preset, whose `NCCL_SOCKET_IFNAME=eth0` is forwarded into
the container by run_xPyD_models.slurm and would override the fabric interface
on an RDMA cluster. `slurm.time` is set explicitly instead, since the
single-node preset's 12 h default is short for a full NIAH sweep.

NIAH model tag. benchmark_niah.sh requested MODEL_PATH as the model name,
correct for the disagg path where vLLM defaults served_model_name to the serve
argument. serve_colocated.sh passes `--served-model-name "$MODEL_NAME"`, so
every request 404'd and all three colocated entries — which default to
BENCHMARK_SCRIPT=niah — would have recorded a full sweep of FAILURE rows. The
harness now honors NIAH_MODEL when a launcher sets it, and the colocated
launcher sets it to the tag it actually serves.

Colocated run metadata. parse_to_csv derived nnodes, n_gpus, deployment_type and
tags from xP/yD. serve_colocated.sh exports xP=1 yD=0 only so the shared log
filenames stay unique, so a 2-node 16-GPU colocated run reported itself as a
1-node 8-GPU `disagg_1P0D` with a nixl backend it never used. Node count now
comes from NNODES (exported by both launchers), and a launcher whose shape is
not "xP prefill + yD decode" states its own identity via PERF_DEPLOYMENT_TYPE /
PERF_TAGS. The disagg path is byte-identical to before.

Also corrects the README, which documented the args-to-sbatch behavior that does
not exist and a quick start that cannot work now that the "<supply-your-image>"
placeholder is rejected rather than pulled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NUM_NODES is derived from the requested topology (xP+yD), not from the
allocation, and the node list was then truncated with `head -n $NUM_NODES`. A
short allocation therefore produced a silently wrong topology: pools were built
from nodes that were never allocated, and the run failed much later as a
connector handshake timeout with no indication of the real cause.

This mattered little while these models were always launched from a matching
`salloc`, but the sbatch path now sizes the allocation from the model card, so a
mismatch between card and cluster is a reachable state. Check up front and name
the three ways to fix it.

The colocated launcher already fails on the equivalent mismatch via its
TP*PP == NNODES*GPUS check, so it needs no counterpart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntract

parse_to_csv.py hand-wrote a full 29-column perf.csv, assembling node counts, GPU
counts, launcher, image and tags from the environment. Most of that is not the
workload's to know: madengine already owns it, and the guesses were wrong for the
colocated launcher, which sets xP=1 yD=0 only to keep log filenames unique.

The NIAH writer now emits a narrow CSV — model, performance, metric, status, plus
descriptive columns — and madengine merges in the run metadata via the entries'
`multiple_results` declaration. The columns that genuinely describe the workload's
own configuration rather than its placement (tp, pp, ep_backend, prefill_decode)
move to descriptive columns, matching how scripts/vllm/run_vllm.py already reports
tp/dtype/bs on the templated path. Nothing is lost; it is relocated to whichever
side actually knows it.

`status` stays explicit: an errored context size scores 0, and deriving status from
performance would file that real failure as a SUCCESS.

Scoped to leave every other workload alone. The NIAH writer is reachable only by the
four Kimi entries, which all declare `multiple_results`. The throughput writer keeps
its full-schema output by default, because the twelve disagg cards that use it
declare no `multiple_results` and madengine reads their CSV directly with no
metadata to merge — a narrow CSV there would drop every descriptive column. Their
output is byte-identical; `--narrow` is the opt-in for migrating one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uted.nnodes

madengine reads only slurm.nodes when emitting `#SBATCH --nodes`
(deployment/slurm.py: `self.nodes = slurm_config.get("nodes", 1)`), and
nothing maps distributed.nnodes across — build_orchestrator copies nnodes
into deployment_config.distributed for launcher detection only. The four
MI300X model cards carried nnodes but no slurm.nodes, so every one of them
would have been submitted as a 1-node job. The launcher's
TP*PP == NNODES*GPUS_PER_NODE assertion catches it, but only after the
allocation is granted.

Add nodes/gpus_per_node to the four slurm blocks, and ship the site configs
that carry the values a model card cannot:

  - results_dir, absent from the key list madengine copies out of models.json,
    and required because the slurm_multi collector globs results_dir for
    perf*.csv rather than resolving multiple_results
  - MODEL_DIR / LOG_PATH, which otherwise default to /shared_inference

*.json is gitignored repo-wide, so the templates need explicit negations or
they never reach a clone.

Correct the two README claims that did not match madengine's behavior
(nnodes-sizes-the-allocation, multiple_results-is-resolved-first), document
that --account/--qos are unwired for slurm_multi and need SBATCH_ACCOUNT,
and drop --keep-model-dir from the SLURM examples — it is a local-Docker
flag that madengine ignores with a warning on SLURM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four defects, each found by running the recipes end to end on a real
MI300X SLURM cluster for the first time. None of them could have survived
a single successful run, which is consistent with these cards never having
been executed through madengine.

1. Build was not reproducible. Every source is pinned to a commit SHA, but
   pip resolves build dependencies in an isolated environment from PyPI at
   build time, so nothing pinned the toolchain. setuptools >= 80 added
       assert isinstance(self.compiler, CCompiler)
   to distutils' build_ext.build_extension, which MoRI's legacy
   Cython.Distutils.build_ext path violates, so the amd_mori wheel failed
   with "AssertionError: run() must precede build_extension()" while every
   pinned SHA was still correct (job 223849). PIP_CONSTRAINT is the only
   mechanism that reaches inside pip's build isolation -- installing
   setuptools in the image does not. Set globally so the AITER, vLLM and
   router stages cannot regress the same way. Build then completed in
   19 minutes, all 18 stages (job 223872).

2. RDMA library mounts used -e, which is true for directories. Docker
   creates a DIRECTORY at a bind-mount source that does not exist, so the
   first run on a node lacking e.g. libionic.so.1 leaves an empty directory
   behind, and every later run on that node mounts a directory over a file
   inside the image:
       OCI runtime create failed: ... not a directory
   Self-propagating and per-node, so it presents as intermittent. The
   surviving node then waits at socket_barrier.py forever for a peer that
   already died -- 4h10m of "Waiting for nodes. . ." before the wall clock
   (job 223909). Use -f so only regular files are mounted.

3. pyt_vllm_kimi-k3_mi300x_pp2xtp8 omitted the gfx942 MoE requirement that
   the two wideep variants carry. gfx942 has no scaled-MXFP4 MFMA and the
   a16w4 SiTUv2 heuristic FlyDSL kernel cannot codegen there, so the MoE
   must be requantized to packed int4. Without it the worker aborts inside
   determine_available_memory with
       LLVM ERROR: Do not know how to expand this operator's operand!
   and quantization_config=None in the engine config (job 224132). This is
   a hardware requirement, not an EP-specific tuning, so all three
   colocated cards now carry AITER_SITUV2_A8W4=1 and the int4 config.

4. A run that produced no results exited 0. The launchers warned about a
   missing perf CSV and returned success anyway, so madengine recorded
   exit_code=0 in its completion marker and the collector reported
   "0 successful, 0 failed" -- a hard engine crash was indistinguishable
   from a clean run. For a benchmark repo that is the worst possible
   reporting outcome. Both launchers now exit 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each of these cost hours to diagnose on a real cluster, and in every case
the symptom is a long way from the cause -- an intermittent multi-node
hang that is actually a poisoned bind-mount path, a wheel build that fails
while every pinned SHA is correct, an LLVM abort that is a missing
quantization flag. Record them so the next person reads instead of
rediscovers.

Also record the cluster facts a model card structurally cannot carry: the
SLURM association wall-time cap (which silently parks a job as PENDING
rather than failing), account requirements, docker on compute nodes, and
the fact that a registry-less cluster has no supported path to distribute
an image -- --build-on-compute requires --registry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lsifiable

Both found on the first end-to-end MI300X run (job 224200), which served
Kimi-K3 and produced results -- but results that could not be trusted, from
a job that then would not exit.

Deadlock on teardown. serve_colocated.sh has the head kill only its own
server and exit, while every worker sits in `wait` on a headless vLLM that
nothing stops. srun waits on those tasks, so the job holds all nodes until
the wall clock: 4.5 hours of two idle exclusive MI300X nodes AFTER the
benchmark had already written perf.csv. The head now raises a sentinel on
the shared log volume and workers watch for it. The sentinel is raised from
an EXIT trap so a head that fails or times out cannot strand workers either.

NIAH could not distinguish truncation from retrieval failure. Scoring reads
content + reasoning_content, and K3 is a reasoning model served with
--reasoning-parser kimi_k3, so a trace that exhausts max_tokens is cut off
and only the earliest needles survive. At the old 2048 default, two of four
context sizes scored 1/10 and both listed exactly ANIMALS[0] -- the needle
placed first in the haystack. That signature is a truncated trace, not a
model failure, but finish_reason was never recorded so the two were
indistinguishable, and every row was written SUCCESS regardless.

  - record finish_reason and print it as finish=<reason>
  - raise the default max_tokens to 8192
  - keep the needle count as the metric, always reported, never dropped
  - mark a truncated row FAILURE and annotate the metric, so a measurement
    artifact cannot enter the results as if it were a model result

9/10 stays SUCCESS: the README documents ~9/10 at >=20K as a known RDMA
residual, so a strict 10/10 gate would flag known-acceptable behaviour.
Logs predating the finish= field still parse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 1, 2026 19:37

Copilot AI 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.

Pull request overview

Adds Kimi-K3 MI300X support by integrating a new colocated multi-node vLLM launcher alongside updates to the existing disaggregated (prefill/decode) vLLM harness, plus reporting/benchmarking and documentation updates to make results consumable by madengine.

Changes:

  • Introduces a 2-node colocated vLLM SLURM+container launcher (vllm_multinode) with shared-model recipe support and shutdown coordination.
  • Extends the disaggregated launcher path to support TP_SIZE>1, multi-node pool host advertisement for MoRIIO, and NIAH benchmarking/reporting.
  • Adds Kimi-K3 model recipes/cards, MI300X-specific docs/site-config templates, and a pinned Dockerfile for the K3 MI300X image.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/vllm_multinode/serve_colocated.sh Per-node in-container entrypoint for colocated multi-node vLLM serving + benchmarking.
scripts/vllm_multinode/run_multinode.slurm SLURM launcher that discovers nodes/IPs, runs per-node containers, and publishes perf CSV.
scripts/vllm_dissag/vllm_disagg.sh Adds TP-aware topology math and peer-pool host export for KV connector.
scripts/vllm_dissag/run_xPyD_models.slurm Adds Kimi-K3 support, TP_SIZE plumbing, NIAH option, safer RDMA mounts, perf CSV publish.
scripts/vllm_dissag/parse_to_csv.py Adds narrow-schema perf CSV mode, NNODES-aware metadata, and NIAH parsing/output.
scripts/vllm_dissag/models.yaml Adds Kimi-K3 MI300X serving recipe env + flags (MXFP4/int4 MoE path).
scripts/vllm_dissag/connectors/moriio.sh Adds multi-node pod host list to KV config; TP-aware api-server-count and tp flag.
scripts/vllm_dissag/benchmark_niah.sh Uses NIAH_MODEL when set; emits perf.csv for NIAH via parse_to_csv.py.
scripts/vllm_dissag/benchmark_niah.py Raises default max_tokens, records finish_reason, and prints parseable result lines.
models.json Adds Kimi-K3 MI300X disagg + colocated model-card entries with multiple_results CSV.
docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile New pinned build for Kimi-K3 MI300X (MoRI/AITER/vLLM/router + graft step).
benchmark/kimi_k3/README.md Notes gfx942 requires multi-node sharding and points to MI300X recipes.
benchmark/kimi_k3/mi300x/slurm-config.disagg.json Site-config template for 4-node disaggregated MI300X runs.
benchmark/kimi_k3/mi300x/slurm-config.colocated.json Site-config template for 2-node colocated MI300X runs.
benchmark/kimi_k3/mi300x/README.md Full MI300X recipe documentation, tradeoffs, and madengine integration notes.
.gitignore Ensures MI300X site-config templates are not ignored.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +105 to +108
if [[ -n "${COLOCATED_EXTRA_ARGS:-}" ]]; then
eval "extra_args=(${COLOCATED_EXTRA_ARGS})"
serve_args+=("${extra_args[@]}")
fi
Comment on lines +149 to +151
docker rm -f $DOCKER_CONT_NAME 2>/dev/null || true
fuser -k 2223/tcp 2>/dev/null || true
sleep 2
Comment thread scripts/vllm_multinode/serve_colocated.sh Outdated
Job 239755 (Kimi-K3, 2x8 on oci-64) died in vLLM's startup barrier. Node 0
finished loading weights at 23:21:41 and entered in_the_same_node_as ->
torch.distributed.barrier; node 1 was still constructing MoE layers ~50 min
later. gloo gave up at 23:51:41, exactly 1800s in, and the master's TCPStore
died with it, so node 1 then failed with "Broken pipe" to :29500. The barrier
is all-or-nothing across all 16 ranks, so what has to stay small is the GAP
between nodes, not the absolute load time.

Five problems, addressed here:

1. --distributed-timeout-seconds does not reach the barrier that failed. It
   feeds ParallelConfig.distributed_timeout_seconds, which only configures the
   device/NCCL groups. The startup barrier runs on the gloo CPU group, built by
   GroupCoordinator from the separate cpu_distributed_timeout_seconds field.
   Unset, it falls back to PyTorch's stock 1800s -- which is exactly the
   deadline in the traceback, despite 7200 being passed. Pass
   --cpu-distributed-timeout-seconds too.

2. Cold AITER JIT compile on every rank, every run. The image points
   AITER_JIT_DIR/TRITON_CACHE_DIR/VLLM_CACHE_ROOT/COMGR_CACHE_DIR at
   /opt/vllm_cache, but this launcher mounted only /tmp/vllm_cache, a path
   nothing reads, inside --rm containers. Mount a host-persistent cache at
   /opt/vllm_cache keyed by image ID, as vllm_dissag/run_xPyD_models.slurm
   already does. Per-node cold compile is precisely the skew the barrier
   cannot absorb.

3. WORKER_PID was tee's PID. After `a | b &` the shell reports b, so every
   kill in this script hit tee and left vLLM running, and no liveness check
   was possible. Launch through process substitution instead.

4. A dead engine was indistinguishable from a slow one. The head polled the
   log for the full 4000s after its writer had already died, then reported a
   timeout rather than the traceback. Both wait loops now check liveness --
   via a helper that also rejects zombies, since an exited-but-unreaped child
   still answers kill -0. This also fixes a latent hang where a worker would
   spin forever on a dead engine.

5. Container env was a closed allowlist. Tuning any of the above from CI meant
   a new -e line and another MAD PR. COLOCATED_FORWARD_ENV names the variables
   to forward, and they are passed via docker --env-file so values containing
   spaces survive. The file is node-local, mode 600, and unlike the wrapper
   scripts is never archived as a build artifact.

Also adds an opt-out checkpoint page-cache pre-warm before the container
barrier (PREWARM_CHECKPOINT=0 to skip). Its per-node duration is the real
diagnostic: a large spread means the storage path is the problem and no
timeout will fix it.

Verified: bash -n clean on both files, and on the single-quoted srun body
extracted in isolation; env-file forwarding preserves spaces and skips unset
vars at mode 600; the liveness helper correctly reports an unreaped child as
dead; DRY_RUN=1 emits the new flag and honours an override, with
COLOCATED_EXTRA_ARGS still appended last.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 3, 2026 14:39

Copilot AI 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.

🟡 Changes recommended

The new run_multinode.slurm launcher has verified set -u-triggered failure paths (optional env expansion and hardcoded barrier port) that can prevent runs from starting or make barrier cleanup inconsistent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

scripts/vllm_multinode/run_multinode.slurm:158

  • The barrier port is configurable via CONTAINER_BARRIER_PORT (and serve_colocated.sh honors it), but this fuser call always kills 2223. If the port is overridden, stale listeners on the configured port won’t be cleared and the barrier can hang/fail. Use the same ${CONTAINER_BARRIER_PORT:-2223} default here.
fuser -k 2223/tcp 2>/dev/null || true
  • Files reviewed: 15/16 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread scripts/vllm_multinode/run_multinode.slurm
Job 240624 spent its entire 7200s allocation in the pre-warm added by the
previous commit. Both nodes printed "[prewarm] reading ..." and neither ever
printed "[prewarm] done", so vllm serve was never launched -- the run timed
out having tested nothing.

The flaw: it reads the WHOLE checkpoint on EVERY node, while PP2xTP8 means a
node only ever loads its own shard. On a 1453 GiB checkpoint that is roughly
4x the necessary I/O against a single NFS export, with both nodes competing
for it. It manufactured exactly the contention it was meant to relieve.

Default it to off. It stays available, because the per-node duration is a
useful storage probe -- a large spread between nodes means the storage path is
the problem and no timeout will fix it -- but it is now opt-in via
PREWARM_CHECKPOINT=1 and bounded by PREWARM_TIMEOUT_SECONDS (default 900). On
expiry it warns that the filesystem could not deliver the checkpoint in the
window and proceeds, rather than burning the whole allocation.

Note this run did not exercise --cpu-distributed-timeout-seconds: vLLM never
reached the startup barrier, so the absence of the 1800000ms gloo timeout in
the log says nothing either way. That fix is still untested.

Verified: default unset is silent; PREWARM_CHECKPOINT=1 runs and reports its
duration; a stalled read hits the budget, warns, and exits 0 so startup
continues.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 3, 2026 18:19

Copilot AI 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.

🔵 Needs a closer look

There are a few correctness/robustness blockers in the new launchers (per-pool host list logic, unsafe eval on args, and a hard-coded barrier port/kill behavior) that should be addressed before relying on these scripts in CI or shared clusters.

Review details

Suppressed comments (5)

Previously missed (3) — in code that hasn't changed since the last review.

scripts/vllm_dissag/vllm_disagg.sh:137

  • PREFILL_POD_HOSTS / DECODE_POD_HOSTS are currently populated whenever either pool spans >1 node. This causes single-node pools (e.g., xP=2,yD=1 or xP=1,yD=2) to still export a non-empty peer list for the 1-node side, which defeats the stated goal of keeping the emitted KV config byte-identical for single-node pools (and can change connector behavior unexpectedly). Populate each host list only when that specific pool spans >1 node.
    scripts/vllm_multinode/run_multinode.slurm:79
  • run_multinode.slurm supports BENCHMARK_SCRIPT=long_context and BENCHMARK_SCRIPT=niah, but the early REQUIRED_FILES check only validates benchmark_xPyD.sh. This means a missing long-context or NIAH script will fail later (inside the container) with a less actionable error. Validate all referenced benchmark scripts up front.
    scripts/vllm_multinode/run_multinode.slurm:247
  • If CONTAINER_BARRIER_PORT is set in the launcher (e.g., to avoid collisions), it is not currently passed into the container environment, so serve_colocated.sh will still default to 2223 inside the container. Pass CONTAINER_BARRIER_PORT via docker run -e so the host-side choice is honored in-container.

scripts/vllm_multinode/serve_colocated.sh:113

  • COLOCATED_EXTRA_ARGS is expanded with eval, which allows arbitrary command execution if the environment value is ever influenced by untrusted input (e.g., CI, wrapper scripts, or user-provided model cards). This can be avoided by using a non-eval parser (e.g., Python shlex.split) to safely produce argv tokens.
if [[ -n "${COLOCATED_EXTRA_ARGS:-}" ]]; then
    eval "extra_args=(${COLOCATED_EXTRA_ARGS})"
    serve_args+=("${extra_args[@]}")
fi

scripts/vllm_multinode/run_multinode.slurm:160

  • The container barrier port is hard-coded to 2223 and the script unconditionally runs fuser -k 2223/tcp. This can kill unrelated processes on non-exclusive nodes and also makes concurrent runs on the same nodes fragile due to port collisions. Consider deriving a job-unique barrier port from SLURM_JOB_ID and using that for both the fuser cleanup and the in-container barrier.
srun --nodelist="$SELECTED_NODELIST" bash -c '
echo "Rank $SLURM_PROCID on $(hostname)"
docker rm -f $DOCKER_CONT_NAME 2>/dev/null || true
fuser -k 2223/tcp 2>/dev/null || true
sleep 2
docker pull $DOCKER_IMAGE_NAME 2>/dev/null || true
  • Files reviewed: 15/16 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Job 432788 died six seconds in:

    run_multinode.slurm: line 272: _RDMA_MOUNTS: unbound variable

naming a variable assigned, plainly, at line 206 -- inside the same srun body.

The whole body is passed to the compute node as a single-quoted string:

    srun --nodelist="$LIST" bash -c '
    ...
    '

and a comment I added during the cluster.sh refactor read "cluster.sh's helper".
That apostrophe closed the string at line 220. Everything after it -- including
$_RDMA_MOUNTS, $_NVME_MOUNT and $_JIT_CACHE_MOUNT -- was then expanded by the
BATCH shell, which has set -u and no such variables. The error prefix said so:
it named run_multinode.slurm, not "bash: -c", but that only helps if you notice.

Not a syntax error, so bash -n accepted it: the quotes re-balanced later in the
file. Comment rewritten without the apostrophe, and a note left in the block
saying why none may appear there.

Added a check for the class. It scans every single-quoted `bash -c` body for an
apostrophe that closes it early, while accepting the two legitimate forms: the
'"$VAR"' interpolation idiom, and a closing line carrying positional arguments
(bash -c '...' _ "$X" "$Y"), which is how serve_colocated.sh passes its prewarm
arguments. Across all 55 shell files in scripts/ it reports zero problems, and
against the parent commit it reports exactly this one.

What the run did prove, before it hit this: the weight probe found Kimi-K3 on
local NVMe on both allocated nodes and selected it over NFS, and the forwarded
env arrived (fwd-env: LOG_WAIT_TIMEOUT_SECONDS). Both fixes work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 11, 2026 18:31

Copilot AI 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.

🟡 Changes recommended

Critical launch, orchestration, and benchmark-reporting issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (10)

Previously missed (1) — in code that hasn't changed since the last review.

scripts/vllm_dissag/connectors/moriio.sh:278

  • The wide-EP command expands model_args at both lines 275 and 278, so every model-specific flag is passed twice. This makes the generated argv harder to reason about and can cause last-occurrence-wins behavior if a recipe later contains a non-idempotent option; keep a single expansion here.

docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile:83

  • The Dockerfile says every source is pinned to immutable commits, but the default AITER donor is a mutable registry tag. Rebuilding this image can silently graft different Python packages even when all repository SHAs are unchanged, defeating the reproducibility guarantee. Pin this FROM source by digest (or record and verify the expected image digest).
ARG PROVEN_K3_IMAGE=amdsiloai/vllm:kimi-k3-mi325x-release-v2

docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile:291

  • The optional WITH_NIXL=1 build is also not reproducible despite the pinning claim: rocSHMEM checks out the moving develop branch and DeepEP checks out its default branch with no ref at all. A future opt-in build can therefore change independently of this PR; pin both repositories to immutable commits (and record the refs in versions.txt).
        cd rocm-systems && git sparse-checkout set --cone projects/rocshmem && git checkout develop && \
        mkdir -p /tmp/rocshmem-build && cd /tmp/rocshmem-build && \
        /tmp/rocm-systems/projects/rocshmem/scripts/build_configs/all_backends \
          -DUSE_EXTERNAL_MPI=OFF -DGPU_TARGETS="${GFX_COMPILATION_ARCH}" && \
      cd /tmp && git clone https://github.com/ROCm/DeepEP.git && cd DeepEP && \

scripts/sglang_disagg/run_xPyD_models.slurm:263

  • cluster_resolve_model_path can select a site-specific MODEL_DIR or MODEL_DIR_CANDIDATES, but the container below only mounts /shared_inference and /mnt/m2m_nobackup. With a custom resolved root, the host-side probe succeeds while every container sees a nonexistent MODEL_PATH, so model loading fails; mount the resolved model root (or reject paths outside the mounted roots) before starting the ranks.
cluster_resolve_model_path || exit 1

scripts/vllm_dissag/benchmark_niah.sh:44

  • The wrapper still injects NIAH_MAXTOK=2048 when the variable is unset, so the new Python default of 8192 is never used in launcher runs. The Kimi-K3 cards do not set NIAH_MAXTOK, leaving reasoning traces truncated and producing the false retrieval misses this change was meant to diagnose. Align this wrapper default with the Python default (or set it explicitly on the K3 cards).
NIAH_MODEL="${NIAH_MODEL:-${MODEL_PATH}}" \
NIAH_WORDS="${NIAH_WORDS:-2000,8000,20000,35000}" \
NIAH_SEEDS="${NIAH_SEEDS:-0,1,2}" \
NIAH_MAXTOK="${NIAH_MAXTOK:-2048}" \

scripts/vllm_dissag/run_xPyD_models.slurm:503

  • This call has the same separate-shell problem: the srun ... bash -c body has not sourced common/cluster.sh, so cluster_mark_dir is undefined and the command fails on every node instead of writing cache metadata. Source or inline the helper before calling it.
    cluster_mark_dir "$_JIT_CACHE_HOST" "JIT/Triton/AITER kernel cache for one docker image; rebuilt on demand"

scripts/vllm_dissag/run_xPyD_models.slurm:263

  • cluster_resolve_model_path can select a site-specific MODEL_DIR or MODEL_DIR_CANDIDATES, but the container below only mounts /shared_inference and /mnt/m2m_nobackup. With a custom resolved root, the host-side probe succeeds while every container sees a nonexistent MODEL_PATH, so model loading fails; mount the resolved model root (or reject paths outside the mounted roots) before starting the ranks.
NIXL_REPO_DIR=$(pwd)

scripts/vllm_multinode/run_multinode.slurm:285

  • The site configuration allows MODEL_DIR/MODEL_DIR_CANDIDATES outside the two default roots, and the host-side probe can successfully select such a path. The container then mounts only fixed /shared_inference plus NVME_ROOT, so an explicit model under another root is invisible inside Docker and startup fails; bind the resolved model parent (or the configured shared roots) as well.
    -v /shared_inference:/shared_inference \

scripts/vllm_multinode/run_multinode.slurm:256

  • cluster_mark_dir is defined only in the batch shell that sourced common/cluster.sh; this command runs in the separate srun bash -c shell and therefore returns command not found on every node. Because the inner shell is not set -e, the job continues but the promised CACHEDIR.TAG/README.ci metadata is never created. Source the helper inside this shell or inline the marker logic.
    cluster_mark_dir "$_JIT_CACHE_HOST" "JIT/Triton/AITER kernel cache for one docker image; rebuilt on demand"

scripts/vllm_multinode/serve_colocated.sh:94

  • This comment says an unset timeout falls back to PyTorch's 1800-second default, but both argv entries explicitly use :-7200 (and cluster.sh also defaults them to 7200). Operators following the comment will underestimate the launcher’s actual wait; document the 7200-second entrypoint default instead.
    # Separate field from the one above: --distributed-timeout-seconds only
    # reaches the device/NCCL groups, while the startup barrier that has been
    # killing colocated runs here is on the gloo CPU group and takes its deadline
    # from this one. Unset, both fall back to PyTorch's stock 1800s.
    --cpu-distributed-timeout-seconds "${CPU_DISTRIBUTED_TIMEOUT_SECONDS:-7200}"
  • Files reviewed: 27/29 changed files
  • Comments generated: 15
  • Review effort level: Lite

libibverbs-dev rdma-core strace libgflags-dev \
libaio-dev liburing-dev libcpprest-dev libgrpc-dev libgrpc++-dev \
libprotobuf-dev protobuf-compiler-grpc wget && \
pip install meson==0.64.0 "pybind11[global]" pyyaml && \
Comment thread scripts/common/cluster.sh
Comment on lines +74 to +75
export TP_SIZE="${TP_SIZE:-${GPUS_PER_NODE}}"
export PP_SIZE="${PP_SIZE:-1}"
Comment on lines +59 to +63
config_match = re.search(r'RUNNING: prompts\s+(?:\d+\s+)?isl\s+(\d+)\s+osl\s+(\d+)\s+con\s+(\d+)', prev_section)
if config_match:
current_input_seq_len = int(config_match.group(1))
current_output_seq_len = int(config_match.group(2))
current_concurrency = int(config_match.group(3))
Comment on lines +143 to +147
counts = [c for c, _ in vals]
n_trunc = sum(1 for _, f in vals if f == "length")
mean = sum(counts) / len(counts)
extra = (" [%d timeout/err excluded]" % n_to) if n_to else ""
if n_trunc:
Comment on lines +53 to +55
python3 "${DIR}/parse_to_csv.py" "${LOG}" --niah \
--perf-csv "/run_logs/${SLURM_JOB_ID}/perf.csv" \
--model-name "${MODEL_NAME}" 2>&1 | tee -a "${LOG}"
Comment on lines +321 to +324
mkdir -p /run_logs/${SLURM_JOB_ID}
SHARED_DIR=$MAD_SCRIPTS_MOUNT/vllm_dissag \
bash $MAD_SCRIPTS_MOUNT/vllm_multinode/serve_colocated.sh \
2>&1 | tee /run_logs/${SLURM_JOB_ID}/colocated_bench_NODE${SLURM_PROCID}.log
Comment on lines +67 to +68
)"
[[ -n "$_yaml_env" ]] && eval "$_yaml_env"
export PERF_DEPLOYMENT_TYPE="${PERF_DEPLOYMENT_TYPE:-colocated_pp${PP_SIZE}xtp${TP_SIZE}}"
export PERF_TAGS="${PERF_TAGS:-vllm_multinode,colocated,${_ep_tag}}"

bash "${SHARED_DIR}/${BENCHMARK_SCRIPT_FILE:-benchmark_xPyD.sh}"
Comment on lines +258 to +262
bash "${SHARED_DIR}/${BENCHMARK_SCRIPT_FILE:-benchmark_xPyD.sh}"

echo "[colocated] benchmark complete; stopping server"
pkill -P "${WORKER_PID}" 2>/dev/null; kill "${WORKER_PID}" 2>/dev/null || true
exit 0
MODEL = os.environ.get("NIAH_MODEL", "")
WORDS = [int(x) for x in os.environ.get("NIAH_WORDS", "2000,8000,20000,35000").split(",") if x.strip()]
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "2048"))
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))
Job 432890 brought both ranks up, passed the container barrier, launched
vllm serve -- and the server died ten seconds later:

    OSError: /lib/x86_64-linux-gnu/libibverbs.so.1: version `IBVERBS_PRIVATE_57`
            not found (required by /lib/x86_64-linux-gnu/libmlx5.so.1)

reported through a Python traceback that names a quantization plugin, because
vLLM loads plugins during startup and that is where the shared library resolves.

The cause is the host-library mount. libibverbs.so.1 is the name everything links
against and it is a symlink. This launcher mounted only the versioned file
(libibverbs.so.1.14.x), which lands the host copy at a path the image symlink does
not point to -- so libibverbs.so.1 still resolved to the image's older copy, while
libmlx5.so.1 came from the pattern loop below it and therefore WAS the host's, and
demanded a symbol only the host libibverbs exports.

scripts/vllm_dissag/run_xPyD_models.slurm has always had a third loop mounting the
sonames themselves, which is why the disagg path has run green. This launcher was
written without it. Ported verbatim.

Also fixed, same commit: cluster_mark_dir was being called inside the srun body in
both this launcher and vllm_dissag. That body is a separate shell on the compute
node where cluster.sh was never sourced, so it failed with "command not found"
once per node -- visible in this run's stderr. The marker is now written inline
there, which is what the comment a few lines below already said to do for the NVMe
mount. Added a check that no cluster.sh function is called from inside any srun
body; all three launchers pass.

What this run proved before it hit the library problem: weights selected from
local NVMe on both nodes, forwarded env delivered, both ranks past the barrier,
and the dead-engine detection reporting "vllm serve exited after 10s" with the
traceback instead of waiting out the 4000s readiness timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 14, 2026 14:11

Copilot AI 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.

🟡 Changes recommended

Critical launcher issues and unresolved benchmark/reporting problems block approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (16)

Previously missed (2) — in code that hasn't changed since the last review.

scripts/vllm_dissag/connectors/moriio.sh:278

  • model_args is appended at line 275 and again at line 278. On the wide-EP Kimi path this repeats every model-specific flag, including the quantization config and maximum model length; remove one occurrence so the generated vLLM command has a single model-argument set.
    scripts/vllm_multinode/serve_colocated.sh:94
  • CPU_DISTRIBUTED_TIMEOUT_SECONDS is defaulted to 7200 on line 94, so the statement that unset timeouts fall back to PyTorch's stock 1800s is false. Document the 7200-second launcher default or actually leave the variable unset; otherwise the timeout behavior is misleading during startup failures.

benchmark/kimi_k3/mi300x/README.md:278

  • The implementation does not currently satisfy this reporting contract: partial seed failures are excluded from the mean and still written with status=SUCCESS, while all-seed failures emit NO-RESULT and produce no row. A timeout regression can therefore be either mislabeled success or silently omitted rather than a FAILURE row with performance 0.
whose request errors is recorded as a `FAILURE` row with performance 0, so a
pass→crash regression shows up instead of silently disappearing.

docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile:291

  • The Dockerfile's PINNING section says every source is immutable, but the optional WITH_NIXL=1 path checks out rocm-systems at the moving develop branch and clones DeepEP without any checkout. That build mode is therefore not reproducible and can change or break independently of the recorded pins; add immutable refs and record them in the image metadata.
      cd /tmp && git clone --no-checkout --filter=blob:none https://github.com/ROCm/rocm-systems.git && \
        cd rocm-systems && git sparse-checkout set --cone projects/rocshmem && git checkout develop && \
        mkdir -p /tmp/rocshmem-build && cd /tmp/rocshmem-build && \
        /tmp/rocm-systems/projects/rocshmem/scripts/build_configs/all_backends \
          -DUSE_EXTERNAL_MPI=OFF -DGPU_TARGETS="${GFX_COMPILATION_ARCH}" && \
      cd /tmp && git clone https://github.com/ROCm/DeepEP.git && cd DeepEP && \

scripts/vllm_dissag/benchmark_niah.py:32

  • The launcher still invokes this script with NIAH_MAXTOK="${NIAH_MAXTOK:-2048}" in benchmark_niah.sh, so this new 8192 default is overridden for every normal harness run unless the model card sets the variable (the Kimi entries do not). Raise the wrapper default or pass 8192 from the Kimi recipes so the intended reasoning headroom is actually used.
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))

scripts/vllm_dissag/benchmark_niah.py:142

  • When every scored request for a context times out, this branch emits only NO-RESULT; parse_to_csv.parse_niah_log recognizes only mean summary lines, so that context disappears from perf.csv instead of becoming the documented FAILURE/zero row. Emit a parseable failure record or update the parser to consume this case.
        if not vals:
            print("  words=%6d  NO-RESULT (%d/%d timed out or errored — likely cold compile; "
                  "raise NIAH_TIMEOUT or keep NIAH_WARMUP=1)" % (n, n_to, len(scored)), flush=True)
            continue

scripts/vllm_dissag/benchmark_niah.py:149

  • The timeout count is written only as a human-readable suffix, while the parser matches the summary prefix and drops that suffix; consequently a context with partial timeouts is emitted with a reduced mean and SUCCESS rather than the documented failure state. Preserve the timeout count in the parsed result and map it to the intended failure/performance behavior.
        extra = ("  [%d timeout/err excluded]" % n_to) if n_to else ""
        if n_trunc:
            # A truncated answer scores low for lack of room, not lack of retrieval.
            extra += "  (TRUNCATED in %d/%d: raise NIAH_MAXTOK)" % (n_trunc, len(vals))

scripts/vllm_dissag/benchmark_niah.sh:44

  • The Python NIAH default was raised to 8192 for reasoning models, but this wrapper still supplies 2048 whenever the caller does not set NIAH_MAXTOK. All three new colocated Kimi cards omit that variable, so their requests retain the truncating limit and the intended fix is bypassed.
NIAH_MAXTOK="${NIAH_MAXTOK:-2048}" \

scripts/vllm_dissag/benchmark_niah.sh:55

  • Now that this wrapper invokes the NIAH parser, a context size whose every seed times out still emits only NO-RESULT; parse_to_csv.py matches no summary row, so that context silently disappears instead of becoming the documented FAILURE row with performance 0. Emit or parse an explicit failure record for all-error contexts.
python3 "${DIR}/parse_to_csv.py" "${LOG}" --niah \
        --perf-csv "/run_logs/${SLURM_JOB_ID}/perf.csv" \
        --model-name "${MODEL_NAME}" 2>&1 | tee -a "${LOG}"

scripts/vllm_dissag/parse_to_csv.py:359

  • The --niah path is not actually narrow: it calls save_niah_perf_csv, which builds the legacy full-schema header and injects _get_run_metadata(). This contradicts the stated multiple_results contract that NIAH rows are workload-only and madengine supplies topology/image metadata, so colocated rows can bypass the authoritative metadata merge.
    parser.add_argument('--narrow', action='store_true',
                        help='Emit a narrow results CSV (model/performance/metric[/status]) for a model card '
                             'declaring multiple_results, letting madengine supply the run metadata. '
                             'Ignored with --niah, which is always narrow.')

scripts/vllm_dissag/run_xPyD_models.slurm:111

  • Kimi-K3 is only added to MORI_EP_VALID_MODELS, but validate_model_name checks VALID_MODELS first (line 144). The new pyt_vllm_disagg_mori_kimi-k3 card therefore exits as invalid before it can reach the allowed MoRI wideEP path; add Kimi-K3 to the primary allowlist as well.
    "Kimi-K3" \

scripts/vllm_multinode/run_multinode.slurm:113

  • long_context is accepted here, but benchmark_long_context.sh only writes benchmark logs and never creates /run_logs/$SLURM_JOB_ID/perf.csv; the unconditional publish step below then exits 1. Either add CSV generation for this mode or reject it instead of advertising it as a valid selector.
    sweep)        BENCHMARK_SCRIPT_FILE="benchmark_xPyD.sh" ;;
    long_context) BENCHMARK_SCRIPT_FILE="benchmark_long_context.sh" ;;
    niah)         BENCHMARK_SCRIPT_FILE="benchmark_niah.sh" ;;

scripts/vllm_multinode/run_multinode.slurm:86

  • cluster.sh initializes MODEL_NAME to the literal None before this expansion, so ${MODEL_NAME:?} never fails when the required variable is omitted; the launcher probes for a model named None instead of reporting the documented missing-env error. Check for empty/None explicitly or remove the sentinel before validating.
MODEL_NAME="${MODEL_NAME:?set MODEL_NAME (key into scripts/vllm_dissag/models.yaml)}"

scripts/vllm_multinode/run_multinode.slurm:334

  • The container receives topology and benchmark settings here, but not LOG_WAIT_TIMEOUT_SECONDS, DISTRIBUTED_TIMEOUT_SECONDS, or CPU_DISTRIBUTED_TIMEOUT_SECONDS from cluster.sh. Site-level timeout overrides are therefore silently lost and serve_colocated.sh falls back to its internal 4000/7200-second values unless users know to list them in COLOCATED_FORWARD_ENV; forward these shared settings explicitly.
    -e TP_SIZE=$TP_SIZE \
    -e PP_SIZE=$PP_SIZE \
    -e ENABLE_EP=$ENABLE_EP \
    ${ALL2ALL_BACKEND:+-e ALL2ALL_BACKEND=$ALL2ALL_BACKEND} \

scripts/vllm_multinode/run_multinode.slurm:365

  • The site configs require the CSV beside scripts/vllm_multinode, but this destination uses the caller's current working directory. A madengine wrapper can submit the script from its job directory, leaving perf_Kimi-K3.csv outside slurm.results_dir so the collector misses an otherwise valid result. Write to ${SCRIPT_DIR} instead.
_PERF_DST="$(pwd)/perf_${MODEL_NAME}.csv"

scripts/vllm_multinode/serve_colocated.sh:252

  • This claim does not match save_niah_perf_csv: NIAH currently writes the full legacy metadata schema, not the narrow workload schema described here and in the Kimi README (for example, it omits benchmark, context_words, tp, pp, and ep_backend). Either emit the documented narrow fields before saying madengine supplies the metadata, or correct the documentation and contract.
#                       Only consulted on the LEGACY full-schema reporting path
#                       (BENCHMARK_SCRIPT=sweep). With BENCHMARK_SCRIPT=niah the CSV
#                       is narrow and madengine supplies these fields itself.
  • Files reviewed: 27/29 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread scripts/common/cluster.sh
Comment on lines +74 to +75
export TP_SIZE="${TP_SIZE:-${GPUS_PER_NODE}}"
export PP_SIZE="${PP_SIZE:-1}"
Comment on lines +314 to +315
-v /shared_inference:/shared_inference \
${_NVME_MOUNT} \
Comment on lines +350 to +353
mkdir -p /run_logs/${SLURM_JOB_ID}
SHARED_DIR=$MAD_SCRIPTS_MOUNT/vllm_dissag \
bash $MAD_SCRIPTS_MOUNT/vllm_multinode/serve_colocated.sh \
2>&1 | tee /run_logs/${SLURM_JOB_ID}/colocated_bench_NODE${SLURM_PROCID}.log
Cemberk and others added 2 commits September 14, 2026 15:48
Two things the first green Kimi run exposed.

WEIGHTS THAT DIFFER PER NODE. On this cluster
/mnt/m2m_nobackup/models_blog/Kimi-K3 is a real 1.5T directory on some nodes and
a symlink to Kimi-K3-MXFP4 on others -- different owners, three weeks apart. The
probe only asked whether a non-empty directory existed, which is true on both, so
a two-node allocation could load two different quantizations under one MODEL_PATH.
Every rank loads from that one path, so the result is wrong numbers, not an error.
Node selection cannot help: the divergence is in the path, not the node, and the
scheduler has no way to express it.

Each node now fingerprints the weights -- md5 of config.json, which is small,
always present in a HuggingFace layout, and carries quantization_config, so
variants of one model differ in it. readlink is reported alongside purely so the
message can say WHY they differ. Disagreement REJECTS that candidate rather than
failing the run, so the walk falls through to shared NFS: one copy every node
reads, uniform by construction. Slower, and correct.

CSV PROVENANCE. The green run published four rows with empty docker_image and
machine_name. Both parse_to_csv.py implementations already read
DOCKER_IMAGE_NAME and SLURM_JOB_NODELIST (_get_run_metadata), but they run inside
the container and neither name was on this launcher's -e list, so they were simply
absent. scripts/sglang_disagg passes both and its rows have always carried the
image tag and nodelist; vllm_dissag passed only the nodelist; vllm_multinode
passed neither. All three now pass both, so a row means the same thing whichever
launcher produced it.

Verified against the real divergence: two nodes with matching config.json ->
NVMe selected; one real directory and one symlink to -MXFP4 -> rejected, both
fingerprints and both resolved paths printed, falls through to NFS and selects it.
Quote, orphan, srun-body and both-paths checks all clean; 16 resolver assertions
unchanged; bash -n clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The variant guard added in the previous commit rejected a divergent NVMe
candidate and fell through to shared NFS. For most models that is the right
trade: slower, correct. For Kimi-K3 it is not.

At 1.5 TB the checkpoint cannot sit in page cache, so an NFS read is served at
NFS speed to every node at once, and at that size the read alone can outlast the
time limit the partition allows. Falling back does not degrade the run; it
produces a job that cannot finish, after holding two nodes for an hour. Failing
immediately, with a message saying which nodes are wrong and how, is strictly
better use of the allocation.

REQUIRE_LOCAL_WEIGHTS=1 makes local roots the only candidates -- by filtering out
anything under SHARED_MOUNT rather than hardcoding a path, so an overridden
MODEL_DIR_CANDIDATES with several local roots still works -- and turns a miss or a
per-node variant mismatch into a hard failure that names the fix.

Set on the four Kimi-K3 cards. Left at 0 everywhere else: it is a property of the
model, not of the site, and the smaller models keep the NFS fallback that has
always worked for them -- Qwen3-32B is not staged on NVMe at all and loads from
NFS in about six seconds.

Verified against the real node layouts: NVMe present and identical -> selected;
NVMe divergent (one real directory, one symlink to -MXFP4) -> fatal, no fallback;
NVMe missing on one node -> fatal; and with the flag off the same layout still
falls back to NFS. Both-paths agreement holds across all 43 cards with the new
key, args -N still equals nnodes equals slurm.nodes, quote/orphan/srun-body
checks clean, bash -n clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 15, 2026 04:46

Copilot AI 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.

🟡 Changes recommended

Unresolved launcher, topology, model-path, and benchmark issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (17)

Previously missed (1) — in code that hasn't changed since the last review.

scripts/vllm_dissag/parse_to_csv.py:359

  • The CLI help says NIAH output is “always narrow,” but save_niah_perf_csv writes the legacy 29-column metadata schema (fieldnames at lines 240-248). This makes the documented --niah output contract inaccurate for callers and model-card integration; either make NIAH honor the narrow schema or describe the actual full-schema output.

scripts/common/cluster.sh:92

  • cluster.sh is sourced before both launchers choose their topology defaults. Exporting TP_SIZE=GPUS_PER_NODE and PP_SIZE=1 here prevents ${TP_SIZE:-1} in vllm_disagg from defaulting to TP1 (all existing wide-EP cards become TP8), and prevents ${PP_SIZE:-${NUM_NODES}} in run_multinode from defaulting to PP across nodes (a 2-node invocation fails the GPU-shape check unless PP_SIZE is explicitly set). Leave these launcher-specific variables unset here.
export TP_SIZE="${TP_SIZE:-${GPUS_PER_NODE}}"
export PP_SIZE="${PP_SIZE:-1}"

scripts/common/cluster.sh:243

  • The REQUIRE_LOCAL_WEIGHTS=1 filter runs before this block, but an explicit MODEL_DIR is appended afterward without the same shared-mount check. A Kimi site config with MODEL_DIR=/shared_inference/... therefore bypasses the local-only policy and can select NFS, contradicting the launcher’s documented guarantee and risking an unfinishable multi-terabyte load. Reject or skip explicit MODEL_DIR values under SHARED_MOUNT when REQUIRE_LOCAL_WEIGHTS is enabled.
    # Append an explicit MODEL_DIR unless the list already covers it.
    if [ -n "${MODEL_DIR:-}" ]; then
        case " ${candidates} " in
            *" ${MODEL_DIR%/} "*) : ;;
            *) candidates="${candidates} ${MODEL_DIR%/}" ;;
        esac

scripts/vllm_dissag/benchmark_niah.py:32

  • The new Python default of 8192 is overridden by benchmark_niah.sh, which still sets NIAH_MAXTOK=${NIAH_MAXTOK:-2048} before invoking this script. Since the Kimi cards do not set NIAH_MAXTOK, these runs still use 2048 and can truncate reasoning responses; update the wrapper default as well.
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))

scripts/vllm_dissag/benchmark_niah.py:145

  • When every seed times out or errors, the preceding if not vals branch prints NO-RESULT and continues, so this summary line is never emitted. parse_niah_log only parses summary lines, meaning that context size disappears instead of becoming the documented FAILURE/0 row; an all-failure run produces no perf.csv at all.
        counts = [c for c, _ in vals]
        n_trunc = sum(1 for _, f in vals if f == "length")
        mean = sum(counts) / len(counts)

scripts/vllm_dissag/benchmark_niah.sh:44

  • The shell wrapper overrides benchmark_niah.py’s new 8192 default with 2048 whenever NIAH_MAXTOK is unset. The Python comments explicitly note that reasoning models need a larger budget, and all three Kimi model cards omit this variable, so their answers can be truncated and scored inaccurately at the lower limit. Keep the wrapper default aligned with the Python default or set it explicitly in the cards.
NIAH_MAXTOK="${NIAH_MAXTOK:-2048}" \

scripts/vllm_dissag/connectors/moriio.sh:275

  • model_args is already appended on line 278 in this same command. Adding it here duplicates every model-specific flag at runtime, including Kimi's --quantization-config and reasoning parser, unlike the dry-run path; duplicate or conflicting single-valued arguments can then change the serve configuration or fail argument parsing. Keep the later append and remove this one.
            "${model_args[@]}" \

scripts/vllm_dissag/run_xPyD_models.slurm:111

  • validate_model_name checks VALID_MODELS at lines 125-144 before the connector-specific allowlist is consulted. Kimi-K3 is added only to MORI_EP_VALID_MODELS here, so the new pyt_vllm_disagg_mori_kimi-k3 card is rejected as invalid before it can launch. Add Kimi-K3 to VALID_MODELS as well.
    "Kimi-K3" \

scripts/vllm_dissag/run_xPyD_models.slurm:715

  • The existing agentic entries in scripts/vllm_dissag/models.json do not declare multiple_results, and common/benchmark_agentic.sh writes agentic artifacts/JSON rather than /run_logs/$SLURM_JOB_ID/perf.csv. This new unconditional check therefore exits 1 for those previously supported DeepSeek agentic cards even when the agentic benchmark succeeds. Restrict the CSV requirement to benchmark modes that produce it, or add an agentic results adapter.
# Publish the perf CSV where madengine collects it. benchmark_xPyD.sh /
# benchmark_niah.sh write /run_logs/$SLURM_JOB_ID/perf.csv inside the container
# (= $LOG_PATH on the host), but madengine resolves a model's `multiple_results`
# against the JOB DIRECTORY it launched us from - so copy it there under the
# declared name. Harmless for entries that do not declare multiple_results.

scripts/vllm_dissag/run_xPyD_models.slurm:294

  • The new resolver accepts an explicit MODEL_DIR/MODEL_PATH, but this launcher only bind-mounts /shared_inference and /mnt/m2m_nobackup into the container. If the documented site config points at another shared or local filesystem, the host probe succeeds while the vLLM container cannot see MODEL_PATH, so the run fails at model load. Bind-mount the explicit model root or reject unsupported paths before starting Docker.
cluster_resolve_model_path || exit 1

scripts/vllm_multinode/run_multinode.slurm:113

  • benchmark_long_context.sh does not invoke parse_to_csv or write /run_logs/$SLURM_JOB_ID/perf.csv, while this launcher unconditionally requires that file before succeeding. Selecting the advertised BENCHMARK_SCRIPT=long_context therefore guarantees a nonzero job even when the benchmark completes. Add a reporting path for this benchmark or remove it from the valid choices until one exists.
    long_context) BENCHMARK_SCRIPT_FILE="benchmark_long_context.sh" ;;
    niah)         BENCHMARK_SCRIPT_FILE="benchmark_niah.sh" ;;

scripts/vllm_multinode/run_multinode.slurm:140

  • Both the master address and the per-node list select the first hostname -I address, but the allocated nodes are multi-homed and cluster.sh provides FABRIC_SUBNET_PREFIX specifically to select the interconnect. If the first address is the management NIC, the rendezvous and barrier addresses are unreachable from peers and startup hangs. Reuse the fabric-subnet selection and fail-fast handling used by the SGLang launcher.
MASTER_ADDR=$(srun --nodes=1 --ntasks=1 --time=00:20:00 --nodelist="$MASTER_NODE" bash -c 'hostname -I' | awk '{print $1}')

IPS=()
for NODE in $SELECTED_NODES; do
    IP=$(srun --nodes=1 --ntasks=1 --time=00:20:00 --nodelist="$NODE" bash -c 'hostname -I' | awk '{print $1}')

scripts/vllm_multinode/run_multinode.slurm:361

  • The command inside the container pipes the launcher through tee without enabling pipefail. If serve_colocated.sh exits nonzero, tee can still return 0 and this rank is reported as successful to Slurm, masking startup failures. Enable set -o pipefail in the nested bash -c script before this pipeline.
          2>&1 | tee /run_logs/${SLURM_JOB_ID}/colocated_bench_NODE${SLURM_PROCID}.log

scripts/vllm_multinode/run_multinode.slurm:315

  • cluster_resolve_model_path accepts an explicit MODEL_DIR/MODEL_PATH, but the container only mounts /shared_inference and the NVMe root. A site config that follows the documented “wherever the checkpoint lives” guidance and uses another filesystem will pass the host-side probe, then leave MODEL_PATH invisible inside Docker and fail model loading. Bind-mount the explicit model root (with a directory guard) or reject unsupported paths before launching.
    ${_NVME_MOUNT} \

scripts/vllm_multinode/serve_colocated.sh:258

  • The default benchmark_xPyD.sh resolves its parser as $NIXL_COOKBOOK_PATH/parse_to_csv.py, but this colocated container sets neither NIXL_COOKBOOK_PATH nor a replacement. It therefore tries /parse_to_csv.py, produces no perf.csv, and the outer launcher reports failure for the default sweep. Set the variable to ${SHARED_DIR} for this invocation or make the benchmark use its own directory.
bash "${SHARED_DIR}/${BENCHMARK_SCRIPT_FILE:-benchmark_xPyD.sh}"

scripts/vllm_multinode/serve_colocated.sh:160

  • This recomputes the local barrier address using the first hostname -I result instead of the rank-ordered address already passed in IPADDRS. On a multi-homed node, peers can dial the fabric address while the barrier binds the management address, so every rank waits indefinitely. Select the address for NODE_RANK from IPADDRS (or use the same fabric resolver).
host_ip=$(hostname -I | awk '{print $1}')

scripts/vllm_multinode/serve_colocated.sh:208

  • When a worker’s vLLM process exits, this loop simply falls through and exits successfully without touching the shared shutdown flag or notifying the head. The head only checks its own PID, so it can continue polling readiness for the full 4000-second timeout after a worker failure, holding the allocation instead of reporting the worker error promptly. Propagate a worker failure to the head and terminate the remaining ranks.
    done
    wait "${WORKER_PID}" 2>/dev/null || true
    exit 0
  • Files reviewed: 27/29 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread scripts/vllm_dissag/run_xPyD_models.slurm Outdated
Comment thread scripts/vllm_multinode/run_multinode.slurm Outdated
Cemberk and others added 2 commits September 15, 2026 17:45
A model card is not always named after its checkpoint. The mad-rccl branch adds
Kimi-K2-Instruct-MoRI-AB and Kimi-K2-Instruct-DeepEP-AB, which differ only in
their all-to-all backend and share one set of weights; its launcher resolves the
directory through a model_weights_name() mapping and passes MODEL_WEIGHTS_NAME.

cluster_resolve_model_path assumed catalog key == directory name, so on that
branch it would look for a directory that does not exist and fail a run whose
weights are present. This is also the larger half of the merge conflict between
that branch and this one: both walk the same three candidates, theirs keyed on
MODEL_WEIGHTS_NAME and ours on MODEL_NAME.

MODEL_WEIGHTS_NAME defaults to MODEL_NAME, so every card that is not an alias
resolves exactly as before and no existing card changes behaviour.

Verified against per-node fake roots: an alias card with MODEL_WEIGHTS_NAME set
resolves to the shared checkpoint; a plain card is unchanged; an alias name with
MODEL_WEIGHTS_NAME unset still fails, and now says "<name> NOT staged" per node
rather than resolving something wrong. Quote, orphan, both-paths and resolver
checks all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fabric block was one cluster's values written as constants: GID index 3,
mlx5_1, and an implied Mellanox driver. That is one row of a table with at least
three. The mad-rccl branch runs the same workloads on AMD AINIC and Broadcom
Thor2, where the RDMA driver, the GID index and the control-plane interface are
all different, and it carries the matrix in
.claude/skills/mad-slurm-multinode/references/cluster-types.md:

    cx7     mlx5_*        GID 3   eth0     mlx5
    ainic   rdma0..7      GID 1   eno0     ionic    + RCCL_AINIC_ROCE=1
    thor2   bnxt_re0..7   GID 3   fenic0   bnxt_re

Getting one wrong does not fail. That file records what happens: RCCL initializes
zero NICs and "silently falls back to TCP sockets -- making the perf number a
measurement of the wrong path". A run that completes and reports a number
measured over the wrong transport is the worst outcome available, and it is the
same class of fault as the weight-variant and NIC-list problems already fixed
here.

CLUSTER_ARCHETYPE is detected from the adapters actually present, because the
node knows and a person guessing does not. Their detect_cluster_env.sh
classifies by the same HCA prefixes; this applies the same rule at source time
rather than proposing values for a human to paste. Everything stays
${VAR:-default}, so naming the archetype or setting any single variable still
wins.

An unclassifiable node resolves to exactly the previous constants, so a node this
cannot identify behaves as it did before rather than differently.

Verified: each archetype produces its own row (cx7 GID 3/eth0/mlx5/mlx5_1; ainic
GID 1/eno0/ionic/rdma0 with RCCL_AINIC_ROCE=1; thor2 GID 3/fenic0/bnxt_re);
unknown reproduces the old values; explicit NCCL_IB_GID_INDEX and KV_IB_DEVICE
override the archetype while the rest still come from it; and detection on this
host correctly returns cx7 from real mlx5_* devices. Quote, orphan, both-paths,
resolver and require-local checks all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 15, 2026 18:23

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

The claim that way 4 is a front end and not a migration is asserted by a unit
test against a synthetic card. This makes it checkable against a real one: the
same workload, expressed by layer instead of as a flat env_vars block, has to
produce the same environment and the same perf_Kimi-K3.csv as the card that ran
green in builds 59 and 60.

The twin declares only the image and a pointer. Everything else -- TP_SIZE,
PP_SIZE, ENABLE_EP, AITER_SITUV2_A8W4, COLOCATED_EXTRA_ARGS, NIAH_WORDS,
REQUIRE_LOCAL_WEIGHTS, MODEL_NAME -- comes from the layered file, split by who
creates it: REQUIRE_LOCAL_WEIGHTS is a site fact (only this cluster's NVMe loads
1.5T in workable time), the serve values belong to whoever tuned the model, and
the NIAH sweep belongs to whoever defines the measurement.

The file is deliberately NOT named mad-config.yaml. That name is picked up by
convention for every card in the directory, which would switch way 4 on for the
two cards that are the control and destroy the comparison. The twin points at it
explicitly through env_vars.MAD_CONFIG, so nothing else in this directory changes
-- which is the same rule the config itself is meant to demonstrate: one way
being present must not disturb the others.

Verified before submitting anything: resolving the twin through
layered_config.resolve_for_model reproduces the original card's nine environment
values exactly, no key added, none missing, none differing.
Copilot AI review requested due to automatic review settings September 16, 2026 04:25

Copilot AI 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.

🟡 Changes recommended

Unresolved critical and moderate findings affect launcher correctness, environment propagation, reporting, and build reproducibility.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (15)

docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile:83

  • This donor image is referenced by a mutable tag even though the Dockerfile claims its sources are pinned for reproducible builds. A tag move can silently change the grafted K3 AITER artifacts without any MAD source change, making benchmark results and rebuilds non-reproducible. Pin the donor image by digest (or record and verify an immutable image identifier).
ARG PROVEN_K3_IMAGE=amdsiloai/vllm:kimi-k3-mi325x-release-v2

docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile:291

  • The optional WITH_NIXL=1 build checks out rocm-systems from the moving develop branch, and the following DeepEP clone has no checkout at all. That contradicts the Dockerfile's reproducibility claim and can change the transport binaries between identical image builds; pin both repositories to immutable commits (and record them in versions.txt).
      cd /tmp && git clone --no-checkout --filter=blob:none https://github.com/ROCm/rocm-systems.git && \
        cd rocm-systems && git sparse-checkout set --cone projects/rocshmem && git checkout develop && \
        mkdir -p /tmp/rocshmem-build && cd /tmp/rocshmem-build && \
        /tmp/rocm-systems/projects/rocshmem/scripts/build_configs/all_backends \
          -DUSE_EXTERNAL_MPI=OFF -DGPU_TARGETS="${GFX_COMPILATION_ARCH}" && \
      cd /tmp && git clone https://github.com/ROCm/DeepEP.git && cd DeepEP && \

docker/sglang_disagg_inference.ubuntu.amd.Dockerfile:37

  • The image pins MoRI to a commit but installs whatever sglang-router is newest at build time. A later router release can change the proxy CLI/API and make the launcher incompatible while the Dockerfile remains unchanged. Pin a tested router version (or commit) alongside the other runtime dependencies.
RUN pip install --upgrade sglang-router

scripts/common/cluster.sh:99

  • These shared exports preempt the launcher-specific topology defaults: vllm_dissag/run_xPyD_models.slurm falls back to TP=1, while the colocated launcher falls back to PP=NNODES. With two nodes and no explicit overrides, run_multinode.slurm preserves TP=8/PP=1 from here and fails its 8 != 16 validation; existing disaggregated jobs also silently become TP=8. Leave both variables unset in the shared site file and let each launcher set its own defaults.
export TP_SIZE="${TP_SIZE:-${GPUS_PER_NODE}}"
export PP_SIZE="${PP_SIZE:-1}"

scripts/vllm_dissag/benchmark_niah.py:32

  • The Python fallback was raised to 8192, but benchmark_niah.sh always exports NIAH_MAXTOK with a 2048 default before invoking this file. Since the Kimi-K3 cards do not override NIAH_MAXTOK, normal launcher runs never use the new 8192 default; update the wrapper default or remove that override.
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))

scripts/vllm_dissag/benchmark_niah.sh:55

  • This new NIAH reporting call exposes two contract bugs in the existing --niah path: it writes the legacy full-schema CSV even though the Kimi-K3 cards use multiple_results/narrow reporting, and save_niah_perf_csv() hard-codes every emitted row as SUCCESS while timeout-only contexts disappear. A failed or partially failed retrieval run can therefore publish an incompatible and falsely successful perf.csv; add a status-aware narrow NIAH writer or align the cards and documentation with the legacy behavior.
python3 "${DIR}/parse_to_csv.py" "${LOG}" --niah \
        --perf-csv "/run_logs/${SLURM_JOB_ID}/perf.csv" \
        --model-name "${MODEL_NAME}" 2>&1 | tee -a "${LOG}"

scripts/vllm_dissag/connectors/moriio.sh:278

  • The wideEP command expands model_args at both lines 275 and 278. Kimi-K3 recipe flags such as --reasoning-parser and --quantization-config are therefore passed twice to every live wideEP server, even though the dry-run branch emits them once. Remove the second expansion.
            "${model_args[@]}" \

scripts/vllm_dissag/parse_to_csv.py:288

  • If every seed times out, benchmark_niah.py emits NO-RESULT and parse_niah_log produces an empty result for that context, so this early return writes no CSV row at all. The Kimi-K3 documentation promises a FAILURE row with performance 0 for this case; preserve the failed context through CSV generation instead of silently dropping it.
    if not results:
        print("No results to save to perf.csv.")
        return

scripts/vllm_dissag/run_xPyD_models.slurm:593

  • Because the preceding line continues the command, this # starts a shell comment while docker run is being assembled. The newline after the comment terminates the Docker command, so the following -e lines are executed as separate commands and the disaggregated container never receives the remaining Docker arguments. Move this note above docker run or outside the continued command.
    # Same reason as the nodelist above: parse_to_csv.py reads this for the
    # docker_image column and runs inside the container, so without it the column
    # is blank on every row this launcher produces.

scripts/vllm_dissag/run_xPyD_models.slurm:111

  • validate_model_name runs before model_allows_mori_ep, so adding Kimi-K3 only to this backend-specific allowlist still rejects MODEL_NAME=Kimi-K3 at line 144. Add it to the global VALID_MODELS list so the new disaggregated card can reach the MoRI-EP validation.
    "Kimi-K3" \

scripts/vllm_multinode/run_multinode.slurm:332

  • Because the preceding line continues the command, this # starts a shell comment while docker run is being assembled. The newline after the comment terminates the Docker command, so the following -e lines are executed as separate commands (-e: command not found) and no container starts. Move this provenance note above docker run or outside the continued command.
    # Provenance for the perf CSV. Both parse_to_csv.py implementations already
    # read these two names (_get_run_metadata -> docker_image, machine_name), but
    # they run INSIDE the container, so a name the -e list omits is simply absent
    # and the column comes out blank. scripts/sglang_disagg passes both and its
    # rows carry the image tag and nodelist; this launcher passed neither, which

scripts/vllm_multinode/run_multinode.slurm:314

  • cluster.sh allows SHARED_MOUNT to be overridden and resolves MODEL_PATH under that root, but this bind mount is hard-coded to /shared_inference. On a site using another shared root, host-side discovery succeeds while the container sees an unmounted model path (or Docker creates an empty directory), so vLLM cannot load the weights. Bind the configured shared root instead.
    -v /shared_inference:/shared_inference \

scripts/vllm_multinode/run_multinode.slurm:112

  • long_context is advertised as a valid selector, but benchmark_long_context.sh ends after the serving loops and never invokes parse_to_csv.py or writes /run_logs/${SLURM_JOB_ID}/perf.csv. The outer launcher unconditionally requires that file before completing, so every long-context run is reported as failed even when the benchmark itself completes. Add its CSV emission or remove this selector.
    long_context) BENCHMARK_SCRIPT_FILE="benchmark_long_context.sh" ;;

scripts/vllm_multinode/run_multinode.slurm:373

  • Unlike the disaggregated launcher, this script never changes to SCRIPT_DIR, but the colocated Slurm config sets results_dir to scripts/vllm_multinode and documents that the CSV is copied beside the launcher. $(pwd) depends on the caller (including the madengine wrapper), so the collector can find no perf_Kimi-K3.csv; write to the already-computed script directory instead.
_PERF_DST="$(pwd)/perf_${MODEL_NAME}.csv"

scripts/vllm_multinode/serve_colocated.sh:258

  • The colocated cards declare multiple_results, and the documented contract says those workload CSVs are narrow, but this shared invocation provides no narrow-reporting mode. benchmark_xPyD.sh calls parse_to_csv.py without --narrow, so a colocated sweep emits the legacy full-schema CSV instead of the declared contract. Add a colocated narrow mode or align the cards and documentation with legacy reporting.
bash "${SHARED_DIR}/${BENCHMARK_SCRIPT_FILE:-benchmark_xPyD.sh}"
  • Files reviewed: 28/30 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment on lines +187 to +188
srun --nodelist="$SELECTED_NODELIST" bash -c '
echo "Rank $SLURM_PROCID on $(hostname)"
-v /tmp/vllm_cache:/tmp/vllm_cache \
$_JIT_CACHE_MOUNT \
$_RDMA_MOUNTS \
--env-file "$_ENV_FILE" \
# Assemble argv
# -----------------------------------------------------------------------------
serve_args=(
--served-model-name "${MODEL_NAME:-model}"
Comment thread scripts/common/cluster.sh
Comment on lines +302 to +308
# Append an explicit MODEL_DIR unless the list already covers it.
if [ -n "${MODEL_DIR:-}" ]; then
case " ${candidates} " in
*" ${MODEL_DIR%/} "*) : ;;
*) candidates="${candidates} ${MODEL_DIR%/}" ;;
esac
fi
…command

Build 61 staged 1.5T of Kimi-K3 weights onto node-local NVMe, verified the
fingerprint matched on both nodes, pulled the image, mounted the JIT cache -- and
then died 56 seconds in with:

    docker: 'docker run' requires at least 1 argument
    /usr/bin/bash: line 148: -e: command not found

I put a comment inside the docker run continuation in c772cdf:

    -e MODEL_NAME=$MODEL_NAME \
    # Provenance for the perf CSV...
    -e DOCKER_IMAGE_NAME=... \

Bash splices the trailing backslash with the next line, so the `#` lands mid
command and ends it there -- docker run with no IMAGE -- and every following
`-e` line then executes as its own command. The comment I added to explain the
provenance fix is what stopped the provenance fix from running.

Both launchers carried it. vllm_multinode showed it because that is the card
being re-run; vllm_dissag has been broken the same way since the same commit and
nobody has run those 17 cards since. Both are fixed here, with the rationale
moved above the command where it cannot be spliced into anything.

This is the second time a comment has changed what executes in these scripts: an
apostrophe inside an srun body closed the string early in build 56 (1e42e8f).
Both are invisible to `bash -n`, which accepts the result as valid -- it simply
is not what was written. So this adds scripts/common/check_continuations.py,
which reads 68 files and fails on any comment inside a continuation. A continued
line that is itself a comment is allowed; the backslash is already inside a
comment and splices nothing.

Verified: the checker reports both offenders before the fix and none after, and
bash -n still accepts both launchers.
Copilot AI review requested due to automatic review settings September 17, 2026 14:39

Copilot AI 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.

🔵 Needs a closer look

Unresolved issues affect launcher defaults, routing, model paths, benchmark results, and image reproducibility.

Review details

Suppressed comments (14)

docker/pyt_vllm_kimi_k3_mi300x.ubuntu.amd.Dockerfile:291

  • The Dockerfile claims every source is pinned to an immutable SHA, but the optional WITH_NIXL=1 path checks out rocm-systems at the moving develop branch and clones DeepEP without any ref. Rebuilding that supported variant can silently change the image even when this Dockerfile is unchanged; pin immutable commits (and record them in versions.txt) for these sources as well.
        cd rocm-systems && git sparse-checkout set --cone projects/rocshmem && git checkout develop && \
        mkdir -p /tmp/rocshmem-build && cd /tmp/rocshmem-build && \
        /tmp/rocm-systems/projects/rocshmem/scripts/build_configs/all_backends \
          -DUSE_EXTERNAL_MPI=OFF -DGPU_TARGETS="${GFX_COMPILATION_ARCH}" && \
      cd /tmp && git clone https://github.com/ROCm/DeepEP.git && cd DeepEP && \

scripts/common/cluster.sh:99

  • These shared exports override the launchers' own defaults: run_xPyD_models.slurm later uses ${TP_SIZE:-1}, so every existing disaggregated run now inherits TP_SIZE=GPUS_PER_NODE (8) instead of TP1, while run_multinode.slurm can never fall back to PP_SIZE=NNODES because this sets PP1. That changes existing disagg topology and makes colocated jobs that omit PP_SIZE fail the GPU-count check. Keep TP_SIZE/PP_SIZE unset here and let each launcher own its defaults.
export TP_SIZE="${TP_SIZE:-${GPUS_PER_NODE}}"
export PP_SIZE="${PP_SIZE:-1}"

scripts/common/cluster.sh:307

  • REQUIRE_LOCAL_WEIGHTS=1 is filtered before this block, but an explicit MODEL_DIR is appended afterward without the same filter. The Kimi cards set this flag, so pointing the site template's MODEL_DIR at /shared_inference silently re-enables the 1.5-TB NFS load despite the guard; reject or ignore shared MODEL_DIR values when local weights are required.
    if [ -n "${MODEL_DIR:-}" ]; then
        case " ${candidates} " in
            *" ${MODEL_DIR%/} "*) : ;;
            *) candidates="${candidates} ${MODEL_DIR%/}" ;;
        esac

scripts/vllm_dissag/benchmark_niah.py:32

  • The implementation default changed to 8192, but the module's NIAH_MAXTOK environment documentation still says the default is 2048. Once the wrapper default is corrected, direct invocations will still be documented incorrectly; update the contract comment alongside this code change.
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))

scripts/vllm_dissag/benchmark_niah.py:32

  • This new Python fallback is not used by the normal harness: benchmark_niah.sh still exports NIAH_MAXTOK="${NIAH_MAXTOK:-2048}" before invoking Python. The Kimi cards do not set NIAH_MAXTOK, so their reasoning responses still run with 2048 tokens and can be truncated, despite this change documenting that 8192 is needed. Raise the shell wrapper default or set the value in the Kimi cards as well.
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))

scripts/vllm_dissag/benchmark_niah.py:149

  • The warning here is only text: parse_niah_log matches the summary prefix and save_niah_perf_csv writes every parsed row with status=SUCCESS. A context with some timeouts is therefore published as a successful mean, while a context whose requests all time out disappears from the CSV entirely. Emit an explicit failure/NaN result (or propagate the timeout status) so a degraded NIAH run cannot be reported as successful.
        if n_trunc:
            # A truncated answer scores low for lack of room, not lack of retrieval.
            extra += "  (TRUNCATED in %d/%d: raise NIAH_MAXTOK)" % (n_trunc, len(vals))

scripts/vllm_dissag/benchmark_niah.sh:44

  • The Python default was raised to 8192, but this wrapper still exports NIAH_MAXTOK with a 2048 default. The Kimi cards do not set NIAH_MAXTOK, so normal Kimi runs continue to hit the 2048-token truncation that the new finish-reason diagnostic calls out. Raise this wrapper default (or set it explicitly in every reasoning-model card).
NIAH_MODEL="${NIAH_MODEL:-${MODEL_PATH}}" \
NIAH_WORDS="${NIAH_WORDS:-2000,8000,20000,35000}" \
NIAH_SEEDS="${NIAH_SEEDS:-0,1,2}" \
NIAH_MAXTOK="${NIAH_MAXTOK:-2048}" \

scripts/vllm_dissag/connectors/moriio.sh:275

  • The new model_args expansion is duplicated by the existing expansion at line 278 in every non-dry-run wideEP command. This makes the real vLLM argv differ from DRY_RUN and passes every model-specific flag twice (including Kimi's quantization and reasoning flags); remove one of the two expansions.
            "${model_args[@]}" \

scripts/vllm_dissag/connectors/moriio.sh:219

  • For Kimi's TP2 disaggregated shape, dp_size here is the pool-wide size (2 nodes × 4 DP ranks = 8), while each master has only DP_PARALLEL_SIZE_LOCAL=4. The comparison therefore leaves --api-server-count=8; clamp against the local DP count, otherwise the frontend can create routes for ranks not served by that master.
            local _api_servers="${_GPUS_PER_NODE}"
            [ "${dp_size}" -lt "${_api_servers}" ] && _api_servers="${dp_size}"
            extra_args+=(--api-server-count=${_api_servers})

scripts/vllm_dissag/run_xPyD_models.slurm:111

  • validate_model_name runs against VALID_MODELS before it reaches MORI_EP_VALID_MODELS, but Kimi-K3 was added only to the latter list. Consequently the new pyt_vllm_disagg_mori_kimi-k3 card always exits as an invalid model before starting its MoRI-EP path. Add Kimi-K3 to the primary valid-model list as well, and keep the gate test mirror in sync.
    "Kimi-K3" \

scripts/vllm_multinode/run_multinode.slurm:113

  • This selector exposes benchmark_long_context.sh, but that script only writes its log and never calls parse_to_csv.py to create /run_logs/$SLURM_JOB_ID/perf.csv. The mandatory post-run check below will therefore fail every BENCHMARK_SCRIPT=long_context colocated job even when the benchmark itself completes. Add the CSV emission to that benchmark or remove this selector until it produces the declared result.
    long_context) BENCHMARK_SCRIPT_FILE="benchmark_long_context.sh" ;;
    niah)         BENCHMARK_SCRIPT_FILE="benchmark_niah.sh" ;;

scripts/vllm_multinode/run_multinode.slurm:328

  • cluster_resolve_model_path can select an explicit MODEL_DIR or a customized SHARED_MOUNT, but the container only mounts the hard-coded /shared_inference and the optional NVME root. With the site templates' documented custom MODEL_DIR, the host-side probe succeeds while the container cannot see MODEL_PATH, so vLLM fails with a missing model. Mount the resolved model root/configured shared root on every node before launching the container.
    -v /shared_inference:/shared_inference \
    ${_NVME_MOUNT} \

scripts/vllm_multinode/serve_colocated.sh:79

  • Serving the colocated instance under MODEL_NAME breaks the shared throughput benchmarks: benchmark_xPyD.sh and benchmark_long_context.sh still invoke vllm bench serve --model "$MODEL_PATH", while only the NIAH wrapper overrides its request model via NIAH_MODEL. Sweep and long-context requests therefore use a model id the server does not serve and fail with 404s. Preserve a single served/request model id across all benchmark scripts.
    scripts/vllm_multinode/serve_colocated.sh:258
  • The shared throughput harness resolves its parser as $NIXL_COOKBOOK_PATH/parse_to_csv.py, but this colocated launcher sets only SHARED_DIR and never defines NIXL_COOKBOOK_PATH. With BENCHMARK_SCRIPT=sweep, parsing consequently targets /parse_to_csv.py and no perf.csv is produced, so the valid sweep fails at the launcher copy check. Export NIXL_COOKBOOK_PATH="$SHARED_DIR" before invoking the shared script, or update the harness to use SHARED_DIR.
  • Files reviewed: 29/31 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Build 72 ran with the build 61 fix in place. The docker errors were gone, weights
were staged on NVMe on both nodes, the image pulled -- and the job still failed in
66 seconds with no container output anywhere and no perf CSV.

The cause was the comment I added in 16e2a04 to explain the build 61 fix. It
quoted that build's error message verbatim:

    #   docker: 'docker run' requires at least 1 argument

That sits inside the srun body, which is ONE single-quoted string. The first
apostrophe closes it; the space in "docker run" is then unquoted, so the shell
splits the word there and bash -c receives a TRUNCATED script with everything from
that point onward as arguments. The executed body ended mid-comment, so the
docker run at the next line was never in it. That is exactly what the logs show:
stdout stops at [fwd-env], the container never starts, and the run fails on the
missing CSV it was never going to produce.

So the fix for build 61's comment-in-a-continuation reintroduced build 56's
apostrophe-in-a-comment. Rather than de-apostrophising in place, all the prose
moves ABOVE the srun, where neither hazard can reach it, and states both rules for
whoever edits this next. Nothing but the body belongs inside the body.

The existing quote checker passed this file. It treated an even apostrophe count
as the deliberate '"$VAR"' splice idiom -- and 'docker run' is even. Parity was
never the right test. What truncates a body is unquoted WHITESPACE in the escaped
text, because that is what splits the shell word; escaped text without whitespace
merges back into the same word and survives, which is why `srun bash -c '...'`
inside a comment in vllm_dissag is harmless and flagging it would have been crying
wolf. check_srun_quotes.py now tests the interior of each pair, and is versioned
here rather than living in /tmp where the last one did.

Calibrated both ways: it reports 16e2a04's line 316 as the offender, and reports
68 files clean afterwards. check_continuations.py still passes, and bash -n
accepts both launchers -- as it did throughout, which is the whole problem with
this class of bug.
STANDALONE has been green since builds 59 and 60; madengine is what is being
brought up now. They share run_multinode.slurm -- Jenkins sbatches it through a
wrapper, madengine sbatches it as slurm_multi -- so a break in that file takes
both down, and I have broken it twice in two days with comments.

Two pieces of evidence that STANDALONE is intact. Comparing EXECUTABLE lines
only (comments and blanks stripped), run_multinode.slurm is unchanged at 188
lines across both recent fixes: 16e2a04 and 9213331 moved prose and changed no
behaviour. Reaching further back, the only executable change since before the
provenance work is the three -e additions it was meant to make:

    run_multinode.slurm     186 -> 188   +DOCKER_IMAGE_NAME  +SLURM_JOB_NODELIST
    run_xPyD_models.slurm   511 -> 512   +DOCKER_IMAGE_NAME

Everything else that moved was a comment, and both of the failures were comments
changing what executes rather than code doing so.

verify_both_paths_generate.py covers the half no test reached. The existing
harness proves the two paths resolve to the same ENVIRONMENT across 44 cards;
this proves madengine can still GENERATE its invocation, which is what builds 62
and 63 could not do. It drives the real generator with a namespaced card name,
asserts the filename is one path segment and writes where build 62 raised ENOENT,
confirms both paths name the same launcher, and runs bash -n plus both hazard
checkers over it.

Four checks now guard this file, and every one of them catches something bash -n
does not: continuations, srun quotes, environment parity, generation parity.
Copilot AI review requested due to automatic review settings September 17, 2026 22:59

Copilot AI 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.

🟡 Changes recommended

Critical and moderate launch, model-resolution, benchmark, and validation issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (12)

Previously missed (1) — in code that hasn't changed since the last review.

scripts/vllm_dissag/connectors/moriio.sh:275

  • model_args is appended both here and again at line 278. This duplicates every model-specific flag in the actual WIDE_EP launch, including Kimi's quantization and reasoning arguments; the DRY_RUN command does not show the duplicate, so the current argv test will not catch it. Keep only the existing later expansion.

benchmark/kimi_k3/README.md:15

  • The overview table contains three framework/model entries (vLLM, SGLang, and ATOM), not four, so this statement is inaccurate. Say “all three” or list the missing fourth model to avoid confusing the MI300X coverage.
> **On MI300X (gfx942)?** All four models above carry `skip_gpu_arch: gfx942` —

scripts/common/cluster.sh:99

  • Because this file is sourced before either launcher applies its own fallback, these exports override both contracts: a default disaggregated run now gets TP_SIZE=8 instead of TP1, while a default colocated 2-node run gets PP_SIZE=1 and then fails the TP*PP == total-GPUs check. Leave these launcher-specific variables unset here.
export TP_SIZE="${TP_SIZE:-${GPUS_PER_NODE}}"
export PP_SIZE="${PP_SIZE:-1}"

scripts/common/cluster.sh:307

  • When REQUIRE_LOCAL_WEIGHTS=1, shared candidates are removed above, but this unconditional append reintroduces an explicit MODEL_DIR under SHARED_MOUNT. A Kimi run with MODEL_DIR=/shared_inference/... will therefore fall back to the NFS checkpoint despite the card's local-only hard-fail requirement. Apply the same local-only filter to this explicit candidate.
    if [ -n "${MODEL_DIR:-}" ]; then
        case " ${candidates} " in
            *" ${MODEL_DIR%/} "*) : ;;
            *) candidates="${candidates} ${MODEL_DIR%/}" ;;
        esac

scripts/vllm_dissag/benchmark_niah.py:145

  • When all seeds for a context time out, this branch emits NO-RESULT and continues without a summary line. The new benchmark_niah.sh parser only recognizes summary lines, so that context is omitted from perf.csv; with partial timeouts, the remaining row is still written as SUCCESS. Emit a failure row/status for missing contexts so a long-context regression cannot pass by omission.
        counts = [c for c, _ in vals]
        n_trunc = sum(1 for _, f in vals if f == "length")
        mean = sum(counts) / len(counts)

scripts/vllm_dissag/benchmark_niah.py:32

  • This new Python default is never used by the normal NIAH launcher: benchmark_niah.sh always sets NIAH_MAXTOK=${NIAH_MAXTOK:-2048} before invoking this file. Therefore Kimi-K3 still runs with 2048 max tokens unless every card overrides NIAH_MAXTOK, despite this change claiming to give reasoning models headroom.
MAXTOK = int(os.environ.get("NIAH_MAXTOK", "8192"))

scripts/vllm_dissag/benchmark_niah.sh:55

  • This newly enabled CSV path does not preserve the failure semantics described by the surrounding documentation: parse_niah_log only recognizes mean summary lines, so an all-timeout context is omitted and a partial-timeout context is still written as SUCCESS by save_niah_perf_csv. Emit an explicit failure/zero row for those contexts, or do not claim that request errors are recorded.
python3 "${DIR}/parse_to_csv.py" "${LOG}" --niah \
        --perf-csv "/run_logs/${SLURM_JOB_ID}/perf.csv" \
        --model-name "${MODEL_NAME}" 2>&1 | tee -a "${LOG}"

scripts/vllm_dissag/parse_to_csv.py:359

  • --narrow is documented here as producing a narrow NIAH CSV, but the --niah path calls save_niah_perf_csv, whose fieldnames are the full legacy metadata schema and whose rows are always SUCCESS. The new NIAH launcher therefore does not produce the narrow multiple_results contract described in the Kimi README; make the NIAH path honor the same schema or correct the contract/documentation.
    parser.add_argument('--narrow', action='store_true',
                        help='Emit a narrow results CSV (model/performance/metric[/status]) for a model card '
                             'declaring multiple_results, letting madengine supply the run metadata. '
                             'Ignored with --niah, which is always narrow.')

scripts/vllm_multinode/run_multinode.slurm:335

  • cluster_resolve_model_path accepts an explicit MODEL_DIR and configurable shared roots, and the site README documents placing the checkpoint wherever the site needs. The container only binds /shared_inference and /mnt/m2m_nobackup, so a valid model selected from any other root is invisible inside Docker and vLLM fails to load it. Mount the selected model root (or reject unsupported paths) before launching.
    scripts/vllm_multinode/run_multinode.slurm:374
  • benchmark_xPyD.sh resolves parse_to_csv.py through NIXL_COOKBOOK_PATH, but this container invocation never sets that variable. The default sweep therefore tries /parse_to_csv.py and produces no perf.csv; pass the mounted vllm_dissag directory as NIXL_COOKBOOK_PATH for the benchmark.
    scripts/vllm_multinode/run_multinode.slurm:112
  • This option selects benchmark_long_context.sh, but that script only runs vllm bench and never invokes parse_to_csv.py. The launcher later unconditionally requires ${LOG_PATH}/${SLURM_JOB_ID}/perf.csv, so selecting the documented long_context mode always ends as a failure for a missing results file.
    scripts/vllm_multinode/serve_colocated.sh:79
  • The server advertises only MODEL_NAME (for example, Kimi-K3), but the shared throughput and long-context benchmarks send MODEL_PATH as their --model request id. Thus every non-NIAH selection receives model-not-found/404 responses; only benchmark_niah overrides its request model. Use one common model id or pass the served name through those benchmark scripts.
  • Files reviewed: 31/33 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment on lines +53 to +55
python3 "${DIR}/parse_to_csv.py" "${LOG}" --niah \
--perf-csv "/run_logs/${SLURM_JOB_ID}/perf.csv" \
--model-name "${MODEL_NAME}" 2>&1 | tee -a "${LOG}"
},
"env_vars": {
"DOCKER_IMAGE_NAME": "<supply-your-image>",
"MAD_CONFIG": "mad-config.kimi-k3.yaml",
Comment on lines +12 to +15
sys.path.insert(0, "/home/madengine/src")

MAD = "/tmp/madtest/MAD_pr242"
CARD_DIR = f"{MAD}/scripts/vllm_multinode"
Comment on lines +45 to +47
target = out / f"madengine_{safe}.sh"
target.write_text("#!/bin/bash\n")
ok2 = target.is_file() and target.parent == out
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants