Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions benchmarks/multi_node/amd_utils/docker_sg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Run `docker` under the 'docker' group.
#
# Why: Slurm launches job steps without the user's 'docker' supplementary group
# in the active credential set. The user IS a docker-group member (getent group
# docker lists them) but the group is missing from `id -G` inside the step, so
# the group-owned socket (/var/run/docker.sock, 0660 root:docker) is unreachable
# via a plain `docker` call. `sg docker -c` re-activates the group for this one
# command — no sudo, no password, no persistent host change.
#
# Used by job.slurm's DOCKER_CMD detection as the fallback when plain `docker`
# fails but `sg docker -c 'docker ps'` succeeds.
#
# argv is passed across the `sg` shell hop by NUL-delimited base64 (NOT string
# re-quoting): naive `printf %q` mangles the big multiline `docker run ... bash
# -lc '<script>'` argument (trailing newline became a literal 'n', spawning a
# stray `$n`). base64 round-trips arbitrary bytes (newlines, quotes) exactly,
# and only the base64 blob (safe chars) is interpolated into the sg command.
b64=$(printf '%s\0' "$@" | base64 | tr -d '\n')
exec sg docker -c "bash -c 'mapfile -d \"\" -t __A < <(printf %s \"$b64\" | base64 -d); exec docker \"\${__A[@]}\"'"
48 changes: 45 additions & 3 deletions benchmarks/multi_node/amd_utils/job.slurm
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,11 @@ SELECTED_NODELIST_STR=$(echo "$SELECTED_NODES" | tr '\n' ',' | sed 's/,$//')

# Docker privilege detection — evaluated per-node since group membership varies.
# Exported as a snippet so every srun participant resolves it locally.
export DOCKER_CMD_DETECT='if docker ps &>/dev/null 2>&1; then DOCKER_CMD=docker; else DOCKER_CMD="sudo docker"; fi'
# Order: (1) plain docker if the socket is reachable; (2) sg-docker wrapper if the
# user is a docker-group member but Slurm didn't activate the group in the step's
# credentials (common here); (3) sudo docker as a last resort.
SG_DOCKER_WRAPPER="${DI_REPO_DIR}/benchmarks/multi_node/amd_utils/docker_sg.sh"
export DOCKER_CMD_DETECT="if docker ps &>/dev/null 2>&1; then DOCKER_CMD=docker; elif sg docker -c 'docker ps' &>/dev/null 2>&1; then DOCKER_CMD='${SG_DOCKER_WRAPPER}'; else DOCKER_CMD='sudo docker'; fi"

# Update SLURM environment variables
export SLURM_NNODES=$NUM_NODES
Expand Down Expand Up @@ -382,7 +386,7 @@ cleanup() {
# on every allocated node. Scoped to $DOCKER_CONT_NAME so it never touches
# other users' containers. (Ported from InferenceY 51ebfa88.)
srun --nodelist="$SELECTED_NODELIST_SRUN" \
bash -c 'eval "$DOCKER_CMD_DETECT"; $DOCKER_CMD rm -f '"$DOCKER_CONT_NAME"' 2>/dev/null || true' 2>/dev/null || true
bash -c 'eval "$DOCKER_CMD_DETECT"; $DOCKER_CMD rm -f '"$DOCKER_CONT_NAME"' 2>/dev/null || true; _pid=$($DOCKER_CMD inspect --format "{{.State.Pid}}" '"$DOCKER_CONT_NAME"' 2>/dev/null || true); if [ -n "$_pid" ] && [ "$_pid" != "0" ]; then sudo kill -9 "$_pid" 2>/dev/null || true; sleep 2; $DOCKER_CMD rm -f '"$DOCKER_CONT_NAME"' 2>/dev/null || true; fi' 2>/dev/null || true
rm -rf ${SLURM_SUBMIT_DIR}/logs 2>/dev/null || true
echo "[${SLURM_JOB_ID}] cleanup done."
}
Expand Down Expand Up @@ -584,6 +588,14 @@ if [[ -n "${CLIENT_IMAGE:-}" ]]; then
srun --nodelist="$SELECTED_NODELIST_SRUN" bash -c 'eval "$DOCKER_CMD_DETECT"; $DOCKER_CMD pull '"$CLIENT_IMAGE"' >/dev/null 2>&1 || true' 2>/dev/null || true
fi

# Pre-pull the main Docker image on every node so the container creation
# barrier (300s) doesn't race against a multi-GB image download on nodes
# where the image isn't cached. Best-effort: failure here is non-fatal
# since docker run will pull as a fallback.
echo "[pre-pull] Pulling $DOCKER_IMAGE_NAME on all nodes..."
srun --nodelist="$SELECTED_NODELIST_SRUN" \
bash -c 'eval "$DOCKER_CMD_DETECT"; echo "[pre-pull] $(hostname): pulling..."; $DOCKER_CMD pull '"$DOCKER_IMAGE_NAME"' && echo "[pre-pull] $(hostname): done" || echo "[pre-pull] $(hostname): pull failed (will retry on docker run)"' || true

srun \
--nodelist="$SELECTED_NODELIST_SRUN" \
--kill-on-bad-exit=1 \
Expand Down Expand Up @@ -681,6 +693,23 @@ fi # end: if ENGINE == atom-disagg
\$DOCKER_CMD ps -aq --filter \"$CONT_FILTER\" | xargs -r \$DOCKER_CMD rm -f || true
\$DOCKER_CMD ps -aq | xargs -r \$DOCKER_CMD stop -t 15 || true
\$DOCKER_CMD ps -aq | xargs -r \$DOCKER_CMD rm -f || true

# Fallback for stuck containers: if docker rm -f fails (\"did not receive an
# exit event\"), the container's shim/init PID is stuck in uninterruptible
# sleep (common after a GPU hang). Kill the container PIDs directly so the
# daemon can clean up the cgroup and release GPU VRAM.
_stuck=\$(\$DOCKER_CMD ps -aq 2>/dev/null || true)
if [[ -n \"\$_stuck\" ]]; then
echo \"[pre-clean] containers still present after docker rm -f; attempting direct PID kill\"
for _cid in \$_stuck; do
_pid=\$(\$DOCKER_CMD inspect --format '{{.State.Pid}}' \"\$_cid\" 2>/dev/null || true)
if [[ -n \"\$_pid\" && \"\$_pid\" != \"0\" ]]; then
sudo kill -9 \"\$_pid\" 2>/dev/null || true
fi
done
sleep 3
\$DOCKER_CMD ps -aq | xargs -r \$DOCKER_CMD rm -f 2>/dev/null || true
fi
sleep 2

# GPU sanity gate: containers are stopped, so any remaining VRAM use is a bare
Expand Down Expand Up @@ -778,7 +807,20 @@ exit \$DOCKER_EXIT_CODE
"

if [[ "${KEEP_CONTAINERS}" != "1" ]]; then
srun --nodelist="$SELECTED_NODELIST_SRUN" bash -c 'eval "$DOCKER_CMD_DETECT"; $DOCKER_CMD rm -f '"$DOCKER_CONT_NAME"' '"$CLIENT_CONT_NAME"' 2>/dev/null || true'
srun --nodelist="$SELECTED_NODELIST_SRUN" bash -c '
eval "$DOCKER_CMD_DETECT"
for _cont in '"$DOCKER_CONT_NAME"' '"$CLIENT_CONT_NAME"'; do
$DOCKER_CMD rm -f "$_cont" 2>/dev/null || true
if $DOCKER_CMD inspect "$_cont" &>/dev/null; then
_pid=$($DOCKER_CMD inspect --format "{{.State.Pid}}" "$_cont" 2>/dev/null || true)
if [ -n "$_pid" ] && [ "$_pid" != "0" ]; then
sudo kill -9 "$_pid" 2>/dev/null || true
sleep 2
$DOCKER_CMD rm -f "$_cont" 2>/dev/null || true
fi
fi
done
' || true

# Clean up vLLM external router container on node 0
if [[ "$ENGINE" == "vllm-disagg" && "$ROUTER_TYPE" == "vllm-router" ]]; then
Expand Down
5 changes: 4 additions & 1 deletion benchmarks/multi_node/amd_utils/models.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ Qwen3.5-397B-A17B-MXFP4:

Qwen3.5-397B-A17B-FP8:
base_flags: "--decode-log-interval 1000 --log-level warning --watchdog-timeout 3600 --load-balance-method round_robin --kv-cache-dtype fp8_e4m3 --attention-backend aiter --disaggregation-transfer-backend mori --moe-dense-tp-size 1"
mtp_flags: ""
# Qwen3.5 built-in MTP head uses EAGLE (in-checkpoint draft, no draft-model-path);
# build_server_config appends --speculative-num-steps=DECODE_MTP_SIZE and
# --speculative-num-draft-tokens=DECODE_MTP_SIZE+1.
mtp_flags: "--speculative-algorithm EAGLE --speculative-eagle-topk 1"
dp_flags: "--enable-dp-attention --enable-dp-lm-head"
ep_flags: "--moe-a2a-backend mori"
prefill:
Expand Down
84 changes: 74 additions & 10 deletions configs/amd-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ qwen3.5-fp8-mi355x-atom-mtp:
- { tp: 8, ep: 1, conc-start: 4, conc-end: 256, spec-decoding: mtp }

qwen3.5-fp8-mi355x-sglang-disagg:
image: lmsysorg/sglang:v0.5.14-rocm720-mi35x
image: lmsysorg/sglang:v0.5.16-rocm720-mi35x
model: Qwen/Qwen3.5-397B-A17B-FP8
model-prefix: qwen3.5
runner: mi355x-disagg
Expand All @@ -277,21 +277,20 @@ qwen3.5-fp8-mi355x-sglang-disagg:
- isl: 8192
osl: 1024
search-space:
# 1P+1D TP8/EP1 low-concurrency sweep.
# dp-attn intentionally false (matches the 1k1k row): with
# --enable-dp-attention + --moe-a2a-backend mori, sglang auto-promotes
# moe_ep_size=tp_size=8, but is_deepep_class_backend() excludes MoRI,
# so num_shared_slots stays at the global value (1) and the
# 1P+1D TP4P+TP8D/EP1 baseline (no speculative decoding).
# TP4 prefill saves 4 GPUs vs TP8P while delivering identical decode
# interactivity and 24-31% better throughput/GPU (12 vs 16 GPUs).
# dp-attn intentionally false: with --enable-dp-attention +
# --moe-a2a-backend mori, sglang auto-promotes moe_ep_size=tp_size,
# but is_deepep_class_backend() excludes MoRI, so
# num_shared_slots stays at the global value (1) and the
# (num_experts - num_shared_slots) % moe_ep_size assertion in
# fused_moe_triton/layer.py fires for Qwen3.5 (512 routed + 1 shared).
# Track upstream sglang for a fix; flip back to dp-attn=true once
# MoRI is added to is_deepep_class_backend() or shared-slot
# accounting is reconciled.
- spec-decoding: "none"
conc-list: [ 8, 16, 32, 64, 128 ]
prefill:
num-worker: 1
tp: 8
tp: 4
ep: 1
dp-attn: false
additional-settings:
Expand All @@ -305,6 +304,71 @@ qwen3.5-fp8-mi355x-sglang-disagg:
- "DECODE_NODES=1"
- "DECODE_MTP_SIZE=0"

# MTP-enabled variant of qwen3.5-fp8-mi355x-sglang-disagg. Same 1P+1D
# TP4P+TP8D/EP1 topology with multi-token prediction on the decode worker
# via spec-decoding=mtp. Qwen3.5 uses the EAGLE speculative algorithm
# (models.yaml mtp_flags); DECODE_MTP_SIZE=3 sets --speculative-num-steps 3,
# --speculative-num-draft-tokens 4, reproducing the validated single-node
# recipe (EAGLE, num-steps 3, draft-tokens 4, topk 1).
# bench.sh auto-adds --use-chat-template for the MTP benchmark (required for
# meaningful acceptance).
qwen3.5-fp8-mi355x-sglang-disagg-mtp:
image: lmsysorg/sglang:v0.5.16-rocm720-mi35x
model: Qwen/Qwen3.5-397B-A17B-FP8
model-prefix: qwen3.5
runner: mi355x-disagg
precision: fp8
framework: sglang-disagg
router: { name: sglang-router, version: "0.3.2" }
kv-p2p-transfer: mori
multinode: true
disagg: true
scenarios:
fixed-seq-len:
- isl: 8192
osl: 1024
search-space:
# 1P+1D TP4P+TP8D/EP1, EAGLE MTP depth 3 — conc 8-64.
- spec-decoding: "mtp"
conc-list: [ 8, 16, 32, 64 ]
prefill:
num-worker: 1
tp: 4
ep: 1
dp-attn: false
additional-settings:
- "PREFILL_NODES=1"
decode:
num-worker: 1
tp: 8
ep: 1
dp-attn: false
additional-settings:
- "DECODE_NODES=1"
- "DECODE_MTP_SIZE=3"
# 1P+1D TP4P+TP4D/EP1, EAGLE MTP depth 3 — conc 128.
# At conc=128, TP8 decode bottlenecks MTP (draft verification
# saturates 8 GPUs, causing -38% tput/GPU and TTFT explosion).
# Switching to TP4D resolves the collapse: +83% tput/GPU, 7.5x
# better TTFT vs TP8D, while interactivity converges (39.5 vs 49.5).
- spec-decoding: "mtp"
conc-list: [ 128 ]
prefill:
num-worker: 1
tp: 4
ep: 1
dp-attn: false
additional-settings:
- "PREFILL_NODES=1"
decode:
num-worker: 1
tp: 4
ep: 1
dp-attn: false
additional-settings:
- "DECODE_NODES=1"
- "DECODE_MTP_SIZE=3"

qwen3.5-fp4-mi355x-sglang:
image: lmsysorg/sglang-rocm:v0.5.16-rocm720-mi35x-20260730
Comment on lines +332 to 373

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 The new qwen3.5-fp8-mi355x-sglang-disagg-mtp entry (configs/amd-master.yaml:332-351) runs decode tp: 8 for the entire conc-list [8,16,32,64,128], but the PR's own results table and perf-changelog.yaml state that at conc=128 TP8 decode causes 'MTP collapse' and that the documented numbers (interactivity 39.5, tput/GPU 5147) were actually measured on TP4P+TP4D. A sweep of this config will therefore reproduce the known-collapse topology at conc=128 instead of the validated one. Consider splitting conc=128 into its own arm with decode tp: 4, matching the pattern used elsewhere in this file (e.g. dsr1-fp8-mi355x-sglang-disagg-mtp).

Extended reasoning...

The committed qwen3.5-fp8-mi355x-sglang-disagg-mtp search-space arm (configs/amd-master.yaml:332-351) is a single block covering conc-list: [8, 16, 32, 64, 128] with decode: { tp: 8, ep: 1, ... } applied uniformly to every concurrency point. There is no second arm carving out conc=128 with a different decode topology.

However, the PR description's own MTP benchmark table daggers the conc=128 row explicitly: 'conc=128 MTP uses TP4P+TP4D (8 GPU) — TP8 decode at conc=128 causes MTP collapse; TP4D resolves it with +83% tput/GPU.' The accompanying perf-changelog.yaml addition echoes this independently: 'at conc=128 the decode TP8 bottleneck causes MTP collapse (-38% tput/GPU), which is resolved by switching to TP4D.' Both artifacts agree the author already discovered and worked around this collapse during local validation, but that workaround (switching decode to TP4 at conc=128) was never encoded back into the committed YAML.

The result is a config/documentation mismatch: the recipe as written will always exercise the TP8 decode topology at conc=128, which is the exact case the author's own text says degrades throughput by ~38%. A sweep run against this config cannot reproduce the row of numbers (interactivity 39.5, tput/GPU 5147) that the PR presents as the MTP conc=128 result, because those numbers came from a TP4D run this config doesn't perform. This isn't a case of stale prose describing correct code — the YAML and the changelog/description are making mutually exclusive factual claims about which topology conc=128 runs on.

This pattern (splitting a conc range into separate arms with different decode tp/DECODE_MTP_SIZE per topology) is already established elsewhere in this same file — see dsr1-fp8-mi355x-sglang-disagg-mtp and dsr1-fp4-mi355x-sglang-disagg-8k1k-mtp, both of which use multiple per-conc arms specifically to match validated topologies. That makes the fix straightforward and consistent with existing conventions: drop conc=128 from the current TP8D arm and add a second arm with conc-list: [128], decode: { tp: 4, ... }, mirroring the existing qwen3.5-fp8-mi355x-sglang-disagg baseline entry's TP4P+TP8D pattern but with decode tp=4.

Concrete walkthrough: (1) a maintainer runs the full sweep on qwen3.5-fp8-mi355x-sglang-disagg-mtp; (2) at conc=128 the recipe launches decode with tp=8 per the single committed arm; (3) per the PR's own analysis this triggers MTP collapse, so the sweep records materially worse throughput (~38% lower tput/GPU per the changelog) than the TP4D figures published in the PR description's table; (4) anyone comparing the recorded sweep data against the PR's stated 5147 tput/GPU for conc=128 will find it doesn't match, because the committed recipe never runs the TP4D topology that produced that number.

Impact is data-quality/reproducibility rather than a hard failure: the run will complete and produce a valid (if degraded) data point rather than crashing, since the sibling spec-none baseline arm also runs conc=128 on TP8D deliberately for apples-to-apples comparison. For that reason this is best treated as a fix-before-merge nit rather than a blocking correctness bug.

model: amd/Qwen3.5-397B-A17B-MXFP4
Expand Down
10 changes: 10 additions & 0 deletions perf-changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5778,6 +5778,16 @@
- "Two search-space arms: TP8/EP1 no-DP at conc-list [2,4,8,16,32], and TP8/EP8/DPA (ep=8, dp-attn=true) at conc-list [64,96,128], both on image lmsysorg/sglang-rocm:v0.5.15.post1-rocm720-mi35x-20260719."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2309

- config-keys:
- qwen3.5-fp8-mi355x-sglang-disagg
- qwen3.5-fp8-mi355x-sglang-disagg-mtp
description:
- "Bump qwen3.5-fp8-mi355x-sglang-disagg image from v0.5.14 to v0.5.16 (3-5% throughput improvement) and switch from TP8P+TP8D (16 GPU) to TP4P+TP8D (12 GPU) — TP4 prefill delivers identical decode interactivity with 24-31% better throughput/GPU."
- "Add qwen3.5-fp8-mi355x-sglang-disagg-mtp: EAGLE speculative decoding (MTP depth 3) on the TP4P+TP8D topology. MTP improves TPOT by 3-14% at conc≤64; at conc=128 the decode TP8 bottleneck causes MTP collapse (-38% tput/GPU), which is resolved by switching to TP4D (see Section 13 of the Qwen3.5 DI perf tuning analysis)."
- "models.yaml: add mtp_flags (--speculative-algorithm EAGLE --speculative-eagle-topk 1) to Qwen3.5-397B-A17B-FP8."
- "Infra: add docker_sg.sh wrapper and update job.slurm DOCKER_CMD_DETECT to use sg-docker fallback for nodes where Slurm doesn't activate the docker supplementary group."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2557

- config-keys:
- dsv4-fp4-b300-vllm-agentic-mtp
description:
Expand Down
Loading