Skip to content
Open
84 changes: 0 additions & 84 deletions benchmarks/multi_node/qwen3.5_fp8_mi355x_sglang-disagg.sh

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# srt-slurm cluster profile for the MI355X AMD Slurm cluster. The login and
# compute nodes share /it-share, so source, output, image, and result paths do
# not require node-local transport.

cluster: mi355x-amds
default_partition: compute
default_time_limit: "01:00:00"
output_dir: /it-share/gharunners2/srt-slurm/outputs

gpus_per_node: 8
accelerator_vendor: amd
network_interface: eno0

gpu_sbatch_directive: gres
use_segment_sbatch_directive: false
use_exclusive_sbatch_directive: false
runtime_config_transport: shared-filesystem

default_sbatch_directives:
cpus-per-task: "32"
mem: "128G"

default_mounts:
/dev/kfd: /dev/kfd
/dev/dri: /dev/dri
/it-share/hf-hub-cache: /hf_hub_cache
# The launcher appends the exact shared workspace and result roots.

containers:
sglang-v0.5.16-mi35x: /it-share/gharunners2/srt-slurm/containers/sglang-rocm-v0.5.16-mi35x-20260728.sqsh

nginx_raise_ulimit: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Production-scale MI355X port of the existing Qwen3.5 FP8 1P1D recipe.
# Each role owns one full 8-GPU node; the native SGLang Router provides the
# request plane and AMD MoRI moves KV directly between the P/D workers.

name: "mi355x-sglang-qwen3.5-fp8-disagg-1p1d-tp4p-tp8d-fixed-seq"

model:
path: "hf:Qwen/Qwen3.5-397B-A17B-FP8"
container: "sglang-v0.5.16-mi35x"
precision: "fp8"

identity:
model:
repo: "Qwen/Qwen3.5-397B-A17B-FP8"
container:
image: "lmsysorg/sglang:v0.5.16-rocm720-mi35x"
frameworks:
sglang: "0.5.16"
sglang-router: "0.3.2"

slurm:
time_limit: "08:00:00"

# This topology owns every GPU on each MI355X node. Give SGLang the complete
# 128-core cpuset as well: its ROCm image enables per-GPU CPU affinity, which
# maps the eight TP ranks across the full dual-socket CPU topology.
sbatch_directives:
cpus-per-task: "128"
mem: "0"

resources:
gpu_type: "mi355x"
gpus_per_node: 8
prefill_nodes: 1
decode_nodes: 1
prefill_workers: 1
decode_workers: 1
gpus_per_prefill: 4
gpus_per_decode: 8

frontend:
type: sglang
enable_multiple_frontends: false
args:
policy: round_robin
prefill-policy: round_robin
decode-policy: round_robin

backend:
type: sglang
prefill_environment: &common_environment
HF_HOME: "/hf_hub_cache"
# Hugging Face stores hub snapshots under $HF_HOME/hub. Keep the explicit
# cache variables on that same path so srt-slurm's prefetch and every
# backend process resolve the identical, current snapshot.
HF_HUB_CACHE: "/hf_hub_cache/hub"
HUGGINGFACE_HUB_CACHE: "/hf_hub_cache/hub"
PYTHONUNBUFFERED: "1"
IBDEVICES: "rdma0,rdma1,rdma2,rdma3,rdma4,rdma5,rdma6,rdma7"
GLOO_SOCKET_IFNAME: "eno0"
NCCL_SOCKET_IFNAME: "eno0"
MORI_RDMA_TC: "104"
MORI_IO_TC: "104"
MORI_IO_SQ_BACKOFF_TIMEOUT_US: "50000"
MORI_IO_QP_MAX_SEND_WR: "16384"
MORI_IO_QP_MAX_CQE: "32768"
MORI_IO_QP_MAX_SGE: "2"
MORI_SHMEM_MODE: "ISOLATION"
SGLANG_USE_AITER: "1"
AITER_LOG_LEVEL: "ERROR"
SGLANG_MORI_DISPATCH_DTYPE: "auto"
SGLANG_MORI_QP_PER_TRANSFER: "4"
SGLANG_MORI_NUM_WORKERS: "4"
SGLANG_DISAGGREGATION_NUM_PRE_ALLOCATE_REQS: "32"
SGLANG_DISAGGREGATION_BOOTSTRAP_TIMEOUT: "3600"
SGLANG_DISAGGREGATION_WAITING_TIMEOUT: "3600"
SGLANG_HEALTH_CHECK_TIMEOUT: "600"
decode_environment: *common_environment
sglang_config:
prefill: &common_config
served-model-name: "Qwen/Qwen3.5-397B-A17B-FP8"
tensor-parallel-size: 4
disaggregation-transfer-backend: mori
disaggregation-ib-device: "rdma0,rdma1,rdma2,rdma3,rdma4,rdma5,rdma6,rdma7"
kv-cache-dtype: "fp8_e4m3"
attention-backend: aiter
moe-dense-tp-size: 1
load-balance-method: round_robin
watchdog-timeout: 3600
decode-log-interval: 1000
log-level: warning
mem-fraction-static: 0.80
max-running-requests: 128
chunked-prefill-size: 262144
context-length: 16384
disable-radix-cache: true
cuda-graph-bs: [1, 2, 3, 4, 5, 6, 7, 8]
Comment on lines +93 to +97

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 recipe's cuda-graph-bs is hard-coded to [1,2,3,4,5,6,7,8] for prefill (and inherited unchanged by decode via the *common_config anchor), even though max-running-requests is 128 and the benchmark sweeps concurrency up to 128. The legacy amd_utils config this replaces (benchmarks/multi_node/amd_utils/models.yaml, Qwen3.5-397B-A17B-FP8, no_dp branch used since dp-attn is false here) used cuda_graph_bs_range: "1-128", expanded via seq 1 128 in server_sglang.sh, i.e. a CUDA graph for every batch size 1-128.

Extended reasoning...

During the c16/c32/c64/c128 points of the sweep (the majority of the 5-point search space), SGLang's running batch size exceeds 8, so no captured CUDA graph exists and the engine silently falls back to eager-mode execution for prefill and decode alike. This produces throughput/latency numbers for most of the sweep that no longer reflect the previously-measured, CUDA-graph-covered production performance -- a silent, undocumented perf regression relative to the pre-migration amd_utils path, despite the PR claiming to 'preserve the production 1P1D TP4P+TP8D topology' and complete search space with parity. Fix: restore cuda-graph-bs to cover 1-128 (or the actual running-request ceiling) for both prefill and decode.

Verification: normal — benchmark-fidelity regression vs base introduced by this change. The new recipe caps CUDA-graph capture at batch size 8 while admitting batches up to 128 and sweeping concurrency to 128: - disagg-1p1d-tp4p-tp8d-fixed-seq.yaml:97 cuda-graph-bs: [1, 2, 3, 4, 5, 6, 7, 8] in the prefill &common_config anchor. - :93 max-running-requests: 128. - :100-102 decode does <<: *common_config

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CUDA graph batch list truncated

High Severity

cuda-graph-bs is [1, 2, 3, 4, 5, 6, 7, 8], but the production Qwen3.5 FP8 no-DP recipe captures graphs for batch sizes 1 through 128. The sweep still runs conc 16/32/64/128 with max-running-requests: 128, so those points miss CUDA graphs and will understate decode throughput versus the current MI355X baseline.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 70228fa. Configure here.

enable-metrics: true
decode:
<<: *common_config
mem-fraction-static: 0.85
prefill-round-robin-balance: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Decode inherits prefill TP4

High Severity

The decode block merges common_config and never overrides tensor-parallel-size, so decode launches with TP4 while gpus_per_decode and the master config still describe TP8. The 1P1D TP4P+TP8D topology this recipe is named for does not actually start.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 32d3ee2. Configure here.

Comment on lines +99 to +102

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.

🔴 BLOCKING: Decode inherits tensor-parallel-size: 4 from the &common_config anchor and never overrides it, so the decode engine is TP4, not the TP8 this PR claims everywhere else (gpus_per_decode: 8 at line 39, the tp8d recipe name, decode.tp: 8 in configs/amd-master.yaml, and the perf-changelog's "one-decode TP8").

Why it matters: Existing srt-slurm sglang recipes (e.g. disagg-b300-1p1d-dep4-dep8.yaml) set tensor-parallel-size explicitly per role, so srt-slurm will not derive it from gpus_per_decode. Best case the run fails on the 4-vs-8 mismatch; worst case decode silently runs TP4 while the launcher stamps result filenames with gen_8/gpus_12 (it computes DECODE_GPUS from the master config's DECODE_TP=8), publishing incorrect perf/GPU numbers.

Fix:

Suggested change
decode:
<<: *common_config
mem-fraction-static: 0.85
prefill-round-robin-balance: true
decode:
<<: *common_config
tensor-parallel-size: 8
mem-fraction-static: 0.85
prefill-round-robin-balance: true

Also double-check the other inherited prefill-oriented values: decode inherits cuda-graph-bs: [1..8], so at the c64/c128 points decode batches above 8 run in eager mode — if that's not intentional, the published high-concurrency points will understate decode throughput.

Comment on lines +80 to +102

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.

🔴 decode config merges *common_config (prefill's tensor-parallel-size: 4) via YAML anchor and never overrides it, so decode runs TP4 instead of TP8 despite the recipe name/gpus_per_decode: 8 and configs/amd-master.yaml decode.tp: 8 specifying TP8 decode.

Extended reasoning...

The benchmark topology silently regresses from the documented '1P TP4 + 1D TP8' to '1P TP4 + 1D TP4' on an 8-GPU decode node: decode either fails to start (TP size mismatch with the 8-GPU allocation/MoRI KV transfer expecting 8 decode ranks) or runs with half the GPUs actually used, producing throughput/latency numbers that do not reflect the intended TP8 decode topology and invalidating the perf-changelog claim of preserving 'the current one-prefill TP4 plus one-decode TP8 topology.'

Verification: normal. The new recipe's decode block (benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/mi355x/disagg-1p1d-tp4p-tp8d-fixed-seq.yaml:100-103) is decode:\n <<: *common_config\n mem-fraction-static: 0.85\n prefill-round-robin-balance: true. It merges &common_config which sets tensor-parallel-size: 4, and never overrides it, so decode inherits TP4. Meanwhile the same file sets…

Comment thread
cursor[bot] marked this conversation as resolved.

srun_options:
container-writable: ""
container-remap-root: ""
mem: "0"

health_check:
max_attempts: 720
interval_seconds: 5

benchmark:
type: custom
command: |
set -euo pipefail
result_root="/results/${SLURM_JOB_ID}"
mkdir -p "${result_root}/fixed-seq"
trap 'tar -C /logs -czf "'"${result_root}"'/runtime-logs.tar.gz" . 2>/dev/null || true' EXIT
for concurrency in 8 16 32 64 128; do
num_prompts=$((concurrency * 10))
if ((num_prompts < 16)); then
num_prompts=16
fi
python3 /infmax-workspace/utils/bench_serving/benchmark_serving.py \
--backend openai-chat \
--base-url "http://${SRT_FRONTEND_HOST}:${SRT_FRONTEND_PORT}" \
--endpoint /v1/chat/completions \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Benchmark backend switched to chat

Medium Severity

The custom client uses --backend openai-chat and /v1/chat/completions. The production AMD sweep called benchmark_serving.py with --backend openai (completions) and no chat template. Chat wrapping changes tokenized ISL/OSL and the router path, so results will not match the prior 8k1k disagg numbers.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 70228fa. Configure here.

--model Qwen/Qwen3.5-397B-A17B-FP8 \
--tokenizer Qwen/Qwen3.5-397B-A17B-FP8 \
--dataset-name random \
--random-input-len 8192 \
--random-output-len 1024 \
--random-range-ratio 1.0 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Benchmark uses exact sequence lengths

Medium Severity

The custom client hardcodes --random-range-ratio 1.0, while the production 8k1k path this replaces always used 0.8 from the multi-node workflow. That changes sampled prompt and output lengths, so the new points are not the same 8k1k workload.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 32d3ee2. Configure here.

--random-num-workers 1 \
--num-warmups "$((concurrency * 2))" \
--num-prompts "${num_prompts}" \
--max-concurrency "${concurrency}" \
--request-rate inf \
--ignore-eos \
--disable-tqdm \
--save-result \
--result-dir "${result_root}/fixed-seq" \
--result-filename "qwen3.5-fp8-disagg-isl8192-osl1024-c${concurrency}.json"
done
env:
HF_HOME: /hf_hub_cache
HF_HUB_CACHE: /hf_hub_cache/hub
HUGGINGFACE_HUB_CACHE: /hf_hub_cache/hub
11 changes: 6 additions & 5 deletions configs/amd-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,24 +285,25 @@ qwen3.5-fp8-mi355x-sglang-disagg:
- isl: 8192
osl: 1024
search-space:
# 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).
# The srt-slurm recipe owns the complete c8-c128 sweep in one Slurm
# allocation so model initialization is paid once instead of per point.
# 1P+1D TP4P+TP8D/EP1 remains the current baseline: TP4 prefill saves
# four GPUs without changing the TP8 decode service.
# 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).
- spec-decoding: "none"
conc-list: [ 8, 16, 32, 64, 128 ]
conc-list: [1]
prefill:
num-worker: 1
tp: 4
ep: 1
dp-attn: false
additional-settings:
- "PREFILL_NODES=1"
- "CONFIG_FILE=recipes/sglang/qwen3.5/mi355x/disagg-1p1d-tp4p-tp8d-fixed-seq.yaml"
decode:
num-worker: 1
tp: 8
Expand Down
14 changes: 13 additions & 1 deletion perf-changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6526,7 +6526,6 @@
- "Pin throughput runs to the committed golden thinking_on acceptance length of 2.32 at three speculative tokens; eval-only runs keep real target verification."
- "Keep the bfloat16 Mamba SSM state the cookbook specifies: SGLang requires it on SM100 or newer whenever the flashinfer linear-attention decode backend is selected."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2758

- config-keys:
- qwen3.8next-fp8-h100-sglang-agentic-mtp
scenario-type:
Expand All @@ -6537,3 +6536,16 @@
- "Use a float32 Mamba SSM state, as Hopper's flashinfer verify kernel requires, unlike the bfloat16 the Blackwell arms must use."
- "Pin throughput runs to the committed golden thinking_on acceptance length of 2.32 at three speculative tokens; eval-only runs keep real target verification."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2756

- config-keys:
- qwen3.5-fp8-mi355x-sglang-disagg
scenario-type:
- fixed-seq-len
description:
- "Replace the model-specific amd_utils launcher with native srt-slurm orchestration using SGLang Router and AMD MoRI."
- "Preserve the current one-prefill TP4 plus one-decode TP8 topology and the 8k1k concurrency 8/16/32/64/128 search space."
- "Run all five points in one eight-hour Slurm allocation through the unchanged benchmark_serving.py custom benchmark."
- "Use the existing lmsysorg/sglang:v0.5.16-rocm720-mi35x image, FP8 KV cache, and AITer attention settings."
- "Pin the launcher to SemiAnalysisAI/srt-slurm PR #7 revision 10a58d2ebb4d756c423424049c736b909b72e14f, including DP-aware SGLang P/D rank routing, multi-node rank-offset preservation, and ATOM recipe flag normalization."
- "Reuse validated MI355X Hugging Face cache trees through the standard $HF_HOME/hub layout under a per-model lock."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2628
Loading
Loading