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
18 changes: 15 additions & 3 deletions benchmarks/multi_node/agentic/dsv4_fp4_mi355x_sglang-disagg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,20 @@ if [[ "$KV_OFFLOADING" != "none" && "${KV_OFFLOAD_BACKEND:-}" == "hicache" ]]; t
export HICACHE_HOST_POOL_COUNT="${HICACHE_HOST_POOL_COUNT:-1}"
# DSV4 uses page-size 256 (set in models.yaml); HiCache must match.
export HICACHE_PAGE_SIZE="${HICACHE_PAGE_SIZE:-256}"
# HiCache ratio (host pool = ratio * GPU KV pool). Default derived in server_sglang.sh.
export HICACHE_RATIO="${HICACHE_RATIO:-}"
# HiCache ratio (host pool = ratio * GPU KV pool).
export HICACHE_RATIO="${HICACHE_RATIO:-4}"
# server_sglang.sh prefers an absolute --hicache-size (derived from
# TOTAL_CPU_DRAM_GB, the sweep generator's per-node DRAM budget) over
# --hicache-ratio whenever TOTAL_CPU_DRAM_GB is set. DSv4 wants the
# ratio-based pool instead. Use FORCE_HICACHE_RATIO to opt out of the
# --hicache-size path rather than unsetting TOTAL_CPU_DRAM_GB itself:
# that var is also the shared client-side gate (benchmark_lib.sh requires
# it to be a positive integer whenever KV_OFFLOADING=dram) and gets
# forwarded into the aiperf sibling container's client.env, so unsetting
# it here made the client fail its own env validation before benchmarking
# ("DRAM KV offloading requires a positive configured TOTAL_CPU_DRAM_GB
# capacity") even though the servers came up fine.
export FORCE_HICACHE_RATIO=1

# ── HiCache layout/backend by tier ──
# L3 (Mooncake): page_first + direct + write_through + storage=mooncake
Expand Down Expand Up @@ -122,7 +134,7 @@ export MORI_IO_SQ_BACKOFF_TIMEOUT_US="${MORI_IO_SQ_BACKOFF_TIMEOUT_US:-500000}"
export MORI_IO_QP_MAX_SEND_WR="${MORI_IO_QP_MAX_SEND_WR:-32768}"

# ── SGLang PD router policy + server metrics ──
export PREFILL_ROUTER_POLICY="${PREFILL_ROUTER_POLICY:-cache_aware}"
export PREFILL_ROUTER_POLICY="${PREFILL_ROUTER_POLICY:-consistent_hashing}"
export ENABLE_METRICS="${ENABLE_METRICS:-1}"

# ── MTP ──
Expand Down
20 changes: 20 additions & 0 deletions benchmarks/multi_node/amd_utils/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,26 @@ else
# FIXME: WA for latest upstream 0305 image
export PYTHONPATH=/sgl-workspace/aiter:${PYTHONPATH}

# Decode CUDA-graph capture crash on ROCm 7.2.0 (TP8+EP8, mori a2a).
# Symptom: during decode cuda-graph capture, the torch ProcessGroupNCCL
# *watchdog* thread calls hipEventQuery() to poll in-flight NCCL work.
# ROCm <= 7.2.0's HIP runtime does NOT honor cudaStreamCaptureModeThreadLocal,
# so the watchdog's cross-thread query touches the main thread's active
# capture and invalidates it -> "HIP error: operation not permitted on an
# event last recorded in a capturing stream (hipErrorCapturedEvent)" ->
# watchdog aborts -> "Rank 0 scheduler died during initialization" (-6).
# This is a HIP runtime bug, not OOM and not a mori/deepep-mode bug (it
# fires for --deepep-mode normal and auto alike; EP8+mori just adds NCCL
# PGs that make the watchdog race fire). Refs: sgl-project/sglang#29235,
# #24011; ROCm/hip#3876; pytorch/pytorch#176251.
# Real fix = ROCm 7.2.2+ (honors THREAD_LOCAL). Until the base image is
# bumped, TORCH_NCCL_BLOCKING_WAIT=true makes NCCL work completion use a
# blocking wait instead of the async watchdog hipEventQuery poll, so no
# event is queried during capture. CUDA graph stays fully enabled.
export TORCH_NCCL_BLOCKING_WAIT="${TORCH_NCCL_BLOCKING_WAIT:-1}"
export NCCL_BLOCKING_WAIT="${NCCL_BLOCKING_WAIT:-1}"
# export NCCL_DEBUG="${NCCL_DEBUG:-INFO}"

# =========================================================================
# DeepSeek-V4-Pro PD recipe overrides
# Placed at the end of the SGLang env block so it wins over the global
Expand Down
38 changes: 38 additions & 0 deletions benchmarks/multi_node/amd_utils/helpers/gpu_sanity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
# GPU sanity gate, run on each node AFTER all containers are stopped.
#
# At that point VRAM should fall back to driver baseline. Any large remaining
# usage means a bare (non-containerized) process is hogging the GPU -- which
# would otherwise surface as an opaque "memory capacity is unbalanced" OOM in
# the middle of model load ~30 min into the job. We fail fast and name the
# offender instead. We only report; we never kill another user's process.
#
# Ported from https://github.com/ROCm/InferenceY/blob/main/benchmarks/multi_node/amd_utils/gpu_sanity.sh
set -uo pipefail

# Fail the node if any single GPU still has >= this many GB used.
GPU_BUSY_GB="${GPU_BUSY_GB:-8}"
# Poll a few times to give docker stop time to release VRAM before judging.
GPU_DRAIN_TRIES="${GPU_DRAIN_TRIES:-6}"

if ! command -v rocm-smi >/dev/null 2>&1; then
echo "[gpu-sanity] rocm-smi not found on $(hostname); skipping GPU check" >&2
exit 0
fi

busy_gb=0
for _ in $(seq "$GPU_DRAIN_TRIES"); do
used=$(rocm-smi --showmeminfo vram --json 2>/dev/null \
| grep -oE '"VRAM Total Used Memory \(B\)": "[0-9]+"' \
| grep -oE '[0-9]+' | sort -n | tail -1)
busy_gb=$(( ${used:-0} / 1024 / 1024 / 1024 ))
if [ "$busy_gb" -lt "$GPU_BUSY_GB" ]; then
exit 0
fi
sleep 5
done

echo "FATAL: $(hostname) GPU still has ${busy_gb}GB used after stopping all containers" >&2
echo " (threshold ${GPU_BUSY_GB}GB) -- a bare (non-containerized) process is hogging the GPU:" >&2
rocm-smi --showpids >&2 || true
exit 1
160 changes: 160 additions & 0 deletions benchmarks/multi_node/amd_utils/helpers/rdma_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/bin/bash
# Pre-flight RDMA QoS/DCQCN validation for a single node, run by job.slurm via
# `srun` across every allocated node BEFORE any container/GPU time is spent.
#
# A misconfigured NIC (PFC not covering the RoCE priority, DCQCN disabled, ...)
# doesn't make MoRI's cross-node EP/RDMA transfers error out -- it just quietly
# degrades, and the only symptom is unexplained tail latency or throughput
# variance in the benchmark numbers hours later. Failing fast here, before the
# job burns node-hours, is much cheaper than debugging that after the fact.
#
# check_qos()/check_dcqcn() below are trimmed/adapted from ROCm/mori's
# tools/env_check.sh:
# https://github.com/ROCm/mori/blob/main/tools/env_check.sh
# The upstream script's expensive ib_write_bw/ib_write_lat full-mesh bandwidth
# and latency tests are intentionally NOT ported here -- this runs before
# every single job, so it must be fast (seconds), not a multi-minute
# fabric-wide benchmark of its own.
#
# Scoped to AMD Pollara (ionic) NICs via nicctl, matching this repo's current
# fleet (see env.sh's nicctl-based MORI_RDMA_TC/SL detection). If bnxt_re or
# mlx5 NICs are added to the fleet, port the bnxt_*/mlx5_* check_* functions
# from the upstream script following the same pattern.
#
# Exit code: 0 = OK (or gracefully skipped, e.g. no ionic NICs on this host),
# 1 = hard QoS/DCQCN misconfiguration -- do not proceed with the run.
set -uo pipefail

AINIC_MIN_VER="1.117.5-a-45" # minimum recommended AINIC firmware for IBGDA

GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m'

log_ok() { echo -e "[$(hostname -s)] ${GREEN}[OK]${NC} $*"; }
log_fail() { echo -e "[$(hostname -s)] ${RED}[FAIL]${NC} $*"; }
log_warn() { echo -e "[$(hostname -s)] ${YELLOW}[WARN]${NC} $*"; }
die() { log_fail "$@"; exit 1; }

# version_ge <candidate> <min> -> true if <candidate> >= <min> (dotted/hyphenated, via sort -V)
version_ge() {
local cand="$1" min="$2"
[[ "$cand" == "$min" ]] && return 0
[[ "$(printf '%s\n%s\n' "$cand" "$min" | sort -V | head -1)" == "$min" ]]
}

# check_ainic_version_recommendation <fw_version>
# warns if firmware is on the IBGDA-incapable 1.117.1 branch, or below the
# recommended minimum for cross-node MORI (EP over RDMA / IBGDA).
check_ainic_version_recommendation() {
local ver="$1"
[[ -n "$ver" ]] || { log_warn "cannot verify AINIC firmware version against recommendation (empty)"; return; }
if [[ "$ver" =~ ^1\.117\.1([.-]|$) ]]; then
log_warn "AINIC firmware $ver is on the 1.117.1 branch, which does NOT support IBGDA -- upgrade to >= $AINIC_MIN_VER"
elif version_ge "$ver" "$AINIC_MIN_VER"; then
log_ok "AINIC firmware $ver meets the recommended minimum (>= $AINIC_MIN_VER) for cross-node IBGDA"
else
log_warn "AINIC firmware $ver is below the recommended minimum (>= $AINIC_MIN_VER) for cross-node IBGDA"
fi
}

# check_versions() -- informational only (never hard-fails the job).
check_versions() {
local fw_output sw_output
fw_output=$(sudo nicctl show version firmware 2>/dev/null)
sw_output=$(sudo nicctl show version host-software 2>/dev/null)

local fw_versions fw_count
fw_versions=$(echo "$fw_output" | grep -i "firmware" | awk '{print $NF}' | sort -u)
fw_count=$(echo "$fw_versions" | grep -c . || true)
if [[ "$fw_count" -ne 1 ]]; then
log_warn "firmware versions not consistent across NICs:"
echo "$fw_versions"
local v
while read -r v; do [[ -n "$v" ]] && check_ainic_version_recommendation "$v"; done <<< "$fw_versions"
else
log_ok "firmware : $fw_versions"
check_ainic_version_recommendation "$fw_versions"
fi

local nicctl_ver
nicctl_ver=$(echo "$sw_output" | grep "nicctl" | awk '{print $NF}')
[[ -n "$nicctl_ver" ]] && log_ok "nicctl : $nicctl_ver" || log_warn "cannot determine nicctl version"
}

# check_qos() -- HARD gate: classification type must be DSCP, and PFC no-drop
# must be enabled and cover every no-drop priority. Dies (exit 1) otherwise.
check_qos() {
local qos_output
qos_output=$(sudo nicctl show qos 2>/dev/null)
[[ -n "$qos_output" ]] || die "sudo nicctl show qos returned nothing"

local class_type
class_type=$(echo "$qos_output" | grep "Classification type" | head -1 | awk '{print $NF}')
[[ "$class_type" == "DSCP" ]] || die "classification type is '$class_type', expected 'DSCP'"
log_ok "classification type : DSCP"

local nd_prio_raw
nd_prio_raw=$(echo "$qos_output" | grep "PFC no-drop priorities" | head -1 | awk '{print $NF}')
[[ -n "$nd_prio_raw" ]] || die "cannot find PFC no-drop priority"
local nd_prios=()
IFS=',' read -ra nd_prios <<< "$nd_prio_raw"
log_ok "no-drop priorities : ${nd_prios[*]}"

local pfc_bitmap
pfc_bitmap=$(echo "$qos_output" | grep "PFC priority bitmap" | head -1 | awk '{print $NF}')
[[ -n "$pfc_bitmap" && "$pfc_bitmap" != "0x0" ]] || die "PFC is not enabled (bitmap=$pfc_bitmap)"
local p
for p in "${nd_prios[@]}"; do
(( pfc_bitmap & (1 << p) )) || die "PFC bitmap $pfc_bitmap does not cover priority $p"
done
log_ok "PFC enabled for priorities ${nd_prios[*]} (bitmap=$pfc_bitmap)"
}

# check_dcqcn() -- HARD gate: DCQCN must be enabled on every ROCE device, and
# the CNP DSCP must be consistent across NICs. Exits 1 otherwise.
check_dcqcn() {
local dcqcn_output
dcqcn_output=$(sudo nicctl show dcqcn 2>/dev/null)
[[ -n "$dcqcn_output" ]] || die "sudo nicctl show dcqcn returned nothing"

local total
total=$(echo "$dcqcn_output" | grep -c "ROCE device")
[[ "$total" -gt 0 ]] || die "no ROCE devices found in dcqcn output"

local disabled
disabled=$(echo "$dcqcn_output" | grep "Status" | grep -v "Enabled" || true)
if [[ -n "$disabled" ]]; then
log_fail "some ROCE devices have DCQCN disabled:"
echo "$disabled"
exit 1
fi
log_ok "DCQCN enabled on all $total ROCE devices"

local cnp_values cnp_count
cnp_values=$(echo "$dcqcn_output" | grep "DSCP value used for CNP" | awk '{print $NF}' | sort -u)
cnp_count=$(echo "$cnp_values" | grep -c . || true)
[[ "$cnp_count" -eq 1 ]] || die "CNP DSCP not consistent across NICs: $cnp_values"
log_ok "CNP DSCP = $cnp_values (consistent across all NICs)"
}

# ============================= main =============================

if ! command -v nicctl &>/dev/null; then
log_warn "nicctl not found on $(hostname -s) -- skipping RDMA QoS/DCQCN pre-flight check (not an ionic NIC host, or nicctl not on PATH)"
exit 0
fi

# nicctl exits 0 even with no NIC present, so check its output rather than its exit code.
_nicctl_probe=$(sudo nicctl show version firmware 2>&1 || true)
if echo "$_nicctl_probe" | grep -qiE 'No AMD NICs|Invalid card handle|Failed to get NIC'; then
log_warn "nicctl present but no ionic NIC detected/accessible on $(hostname -s) -- skipping RDMA QoS/DCQCN pre-flight check"
exit 0
fi

check_versions
check_qos
check_dcqcn

log_ok "RDMA QoS/DCQCN pre-flight check passed on $(hostname -s)"
30 changes: 30 additions & 0 deletions benchmarks/multi_node/amd_utils/job.slurm
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,29 @@ export RUN_FILE_FULL="$WS_PATH/${RUN_FILE}"

SELECTED_NODELIST_SRUN=$(echo "$SELECTED_NODES" | paste -sd,)

# =============================================================================
# RDMA QoS / DCQCN Pre-flight Check
# =============================================================================
# Gate the run on NIC QoS (PFC/DSCP) and DCQCN config on every node before
# any container/GPU time is spent. Runs on the bare host (nicctl is a host
# tool). See rdma_check.sh for details.
RDMA_CHECK_SCRIPT="$(pwd)/helpers/rdma_check.sh"
if [[ "${SKIP_RDMA_CHECK:-0}" == "1" ]]; then
echo "[INFO] SKIP_RDMA_CHECK=1 set; skipping RDMA QoS/DCQCN pre-flight check"
elif [[ -f "$RDMA_CHECK_SCRIPT" ]]; then
echo "Checking RDMA QoS/DCQCN configuration on all $NUM_NODES allocated node(s)..."
srun --nodelist="$SELECTED_NODELIST_SRUN" --ntasks=$NUM_NODES bash "$RDMA_CHECK_SCRIPT"
RDMA_CHECK_RC=$?
if [[ $RDMA_CHECK_RC -ne 0 ]]; then
echo "FATAL: RDMA QoS/DCQCN pre-flight check failed on one or more nodes (see [FAIL] lines above)."
echo " Set SKIP_RDMA_CHECK=1 to bypass (not recommended -- MoRI cross-node transfers would run unprotected by PFC/DCQCN)."
exit 1
fi
echo "RDMA QoS/DCQCN pre-flight check passed on all $NUM_NODES node(s)"
else
echo "[WARN] $RDMA_CHECK_SCRIPT not found; skipping RDMA QoS/DCQCN pre-flight check"
fi

cleanup() {
echo "[${SLURM_JOB_ID}] termination received on $(hostname); cleaning up container + stale logs..."
# Backstop: on scancel/timeout/step-hang the foreground `exec docker run`
Expand Down Expand Up @@ -494,6 +517,7 @@ fi
HICACHE_MC_CONFIG="${BENCHMARK_LOGS_DIR}/hicache_mc_${SLURM_JOB_ID}.env"
cat > "$HICACHE_MC_CONFIG" <<EOF
HICACHE_RATIO=${HICACHE_RATIO:-}
FORCE_HICACHE_RATIO=${FORCE_HICACHE_RATIO:-}
HICACHE_HOST_POOL_COUNT=${HICACHE_HOST_POOL_COUNT:-}
HICACHE_PAGE_SIZE=${HICACHE_PAGE_SIZE:-}
HICACHE_IO_BACKEND=${HICACHE_IO_BACKEND:-}
Expand Down Expand Up @@ -635,6 +659,12 @@ 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 || true

# GPU sanity gate: containers are stopped, so any remaining VRAM use is a bare
# (non-containerized) process hogging the GPU -- fail fast (and name it)
# instead of OOMing in model load ~30 min later. set -e + --kill-on-bad-exit
# tears down the whole job on non-zero exit.
bash \"$DI_REPO_DIR/benchmarks/multi_node/amd_utils/helpers/gpu_sanity.sh\"

# Start vLLM external router container on node 0
if [[ \"$ENGINE\" == \"vllm-disagg\" && \"$ROUTER_TYPE\" == \"vllm-router\" && \"\$SLURM_PROCID\" == \"0\" ]]; then
\$DOCKER_CMD rm -f \"$ROUTER_CONT_NAME\" 2>/dev/null || true
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/multi_node/amd_utils/models.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -365,19 +365,19 @@ DeepSeek-R1-0528-MXFP4-v2:

DeepSeek-V4-Pro-AgentX:
base_flags: "--watchdog-timeout 3600 --load-balance-method round_robin --kv-cache-dtype fp8_e4m3 --attention-backend dsv4 --page-size 256 --swa-full-tokens-ratio 0.1 --disable-shared-experts-fusion --tool-call-parser deepseekv4 --reasoning-parser deepseek-v4 --disaggregation-transfer-backend mori --log-level info --log-level-http error"
dp_flags: "--enable-dp-attention --enable-prefill-delayer --enable-two-batch-overlap"
dp_flags: "--enable-dp-attention --enable-prefill-delayer"
ep_flags: "--ep-dispatch-algorithm fake --moe-a2a-backend mori --deepep-mode normal"
prefill:
mem_fraction_static: 0.8
mem_fraction_static: 0.72
disable_radix_cache: false
disable_cuda_graph: false
disable_cuda_graph: true
dp:
max_running_requests: 1024
chunked_prefill_size: "MORI_MAX_DISPATCH_TOKENS_PREFILL * PREFILL_TP_SIZE" # dsv4 compressor kernel uint16 token cap (255*256)
context_length: 1048576
# max_total_tokens: 1048576
no_dp:
max_running_requests: 128
max_running_requests: 64
# Small prefill chunks interleave long-context agentic prefills across
# requests instead of letting one ~100K-token prefill monopolize the
# engine (the conc>=16 queue-saturation / decode-stall failure mode).
Expand Down
Loading