MORI UMBP PD Disaggregation Benchmark
Runs a prefill-decode disaggregated serving benchmark using SGLang with mori’s UMBP KV-cache transfer backend.
This guide is xP1D-generic: N prefill nodes (N >= 1) plus one decode node.
The 1P1D case is just N=1; the 2P1D case is N=2. Set PREFILL_NODES /
PREFILL_IPS to drive the topology — every step below loops over them.
For the architecture this benchmark exercises (master-as-advisor,
heartbeat-event index, peer-owned allocator), see
src/umbp/doc/design-master-control-plane.md.
Every UMBP_* env var referenced below is documented in
src/umbp/doc/runtime-env-vars.md.
Table of Contents
Prerequisites
N+1 nodes with ROCm-capable GPUs (8 GPUs per node, dp8ep8 topology): N prefill nodes + 1 decode node (N >= 1)
NFS mount shared between all nodes (for logs, results, and mori/sglang source)
SSH key access from the local machine to all nodes
Docker with ROCm image available on all nodes
mori built with
UMBP=ON(Step 3 below)SGLang checked out at
$NFS_BASE/sglangwithbenchmark/hicache/run_pd_disagg_bench_dp8ep8.shsupporting bothPREFILL_URLS(space-separated N prefill URLs) and thewait_for_router_workersgate (polls/get_loadsafter router is ready)
Topology
┌──────────────────────────────────┐
│ PREFILL NODE [0] (PRIMARY) │
│ │
│ SGLang prefill server :30000 │
│ UMBP master :15558 ◄────────────┐
│ KV events publisher :5557 │ │
└──────────────────────────────────┘ │
│
┌──────────────────────────────────┐ │ ┌──────────────────────────────────┐
│ PREFILL NODE [i] (i>=1) │ │ │ DECODE NODE │
│ (only present when N >= 2) │ │ │ │
│ │ │ │ SGLang decode server :30001 │
│ SGLang prefill server :30000 │ ├──►│ sglang_router :8000 │
│ UMBP_MASTER_AUTO_START=false ──┼──────────┤ │ benchmark client ────┼─► localhost:8000
│ KV events publisher :5557 │ │ │ UMBP_MASTER_AUTO_START=false ───┘
└──────────────────────────────────┘ │ │ KV events publisher :5557 │
│ │ │
│ │ Grafana :3000 │
│ │ Prometheus :9090 │
│ └──────────────────────────────────┘
│
(UMBP master discovery / dp-rank registry)
All nodes share a single UMBP master that runs on PREFILL_NODES[0]
(the primary prefill). The primary’s bench script auto-starts the master;
all other prefills (i>=1) and the decode node set
UMBP_MASTER_AUTO_START=false and connect to it. With N prefills + 1 decode
this means (N+1) * 8 = 8(N+1) dp ranks all register with one master.
The decode node hosts sglang_router (started by the bench script). The
router fans out to all N prefills via repeated --prefill flags driven by
the PREFILL_URLS env var.
Configuration
Define these variables once before running any step. All subsequent code blocks
refer to them. PREFILL_NODES and PREFILL_IPS are bash arrays of equal
length — set one entry for 1P1D, two for 2P1D, N for xP1D.
# === Edit for your environment ===
USER_HOME="/home/youruser" # home directory (same path on all nodes via NFS)
NFS_BASE="/nfs/users/youruser" # NFS root containing sglang/ and mori/
SSH_KEY="$USER_HOME/.ssh/id_ed25519" # SSH private key for node access
# Prefill nodes (xP1D). Index 0 is the PRIMARY: it hosts the UMBP master and
# must be started first. Indices 1..N-1 are SECONDARY: they connect to the
# primary's master. Both arrays MUST have the same length.
#
# 1P1D example:
# PREFILL_NODES=("node-prefill-1")
# PREFILL_IPS=("10.x.x.1")
#
# 2P1D example:
# PREFILL_NODES=("node-prefill-1" "node-prefill-2")
# PREFILL_IPS=("10.x.x.1" "10.x.x.2")
PREFILL_NODES=("node-prefill-1")
PREFILL_IPS=("10.x.x.1")
NODE_DECODE="node-hostname-decode" # decode node hostname
IP_DECODE="10.x.x.y" # decode node IP
# Set to the image matching your AMD GPU platform, e.g.:
# MI300X / MI325X (gfx942): rocm/sgl-dev:vX.Y.Z-rocm7XX-mi30x-YYYYMMDD
# MI350X (gfx950): rocm/sgl-dev:vX.Y.Z-rocm7XX-mi35x-YYYYMMDD
DOCKER_IMAGE="rocm/sgl-dev:v0.5.9-rocm700-mi30x-20260316"
# Network interfaces — find with: ibstat | grep CA; ip link show
NCCL_IB_HCA="ionic_0,ionic_1,ionic_2,ionic_3,ionic_4,ionic_5,ionic_6,ionic_7"
NET_IFNAME="ens14np0" # TCP socket interface for GLOO/NCCL/MORI
MORI_RDMA_DEVICES="^mlx5_8" # use ^ prefix to exclude; leave empty to use all devices
# Benchmark parameters
OUTPUT_LENGTH=100 # decode output tokens per request
# Extra Docker volume mounts (site-specific; clear if not needed)
EXTRA_MOUNTS=""
# Example: EXTRA_MOUNTS="-v /data/models:/models -v /apps:/apps"
# =================================
# === Derived (do not edit) ===
# Primary prefill: hosts the UMBP master.
NODE_PREFILL_PRIMARY="${PREFILL_NODES[0]}"
IP_PREFILL_PRIMARY="${PREFILL_IPS[0]}"
N_PREFILLS=${#PREFILL_NODES[@]}
# Sanity check arrays are same length and non-empty.
if (( N_PREFILLS == 0 )) || (( N_PREFILLS != ${#PREFILL_IPS[@]} )); then
echo "ERROR: PREFILL_NODES and PREFILL_IPS must be non-empty and same length" >&2
return 1 2>/dev/null || exit 1
fi
SSH="ssh -o StrictHostKeyChecking=no -i $SSH_KEY"
SCP="scp -o StrictHostKeyChecking=no -i $SSH_KEY"
CONTAINER="umbp-pd-bench"
RESULTS_BASE="$NFS_BASE/sglang/benchmark/hicache/results"
# All nodes (used by Steps 1, 3, Teardown).
ALL_NODES=( "${PREFILL_NODES[@]}" "$NODE_DECODE" )
Step 1 — Start Docker on all nodes
Loops over every prefill node plus the decode node.
for NODE in "${ALL_NODES[@]}"; do
$SSH $NODE "
docker rm -f $CONTAINER 2>/dev/null || true
docker run -d --name $CONTAINER \
--ulimit memlock=-1:-1 --ulimit stack=67108864:67108864 \
--device /dev/dri --device /dev/kfd \
--network host --ipc host --group-add video \
--cap-add SYS_PTRACE --security-opt seccomp=unconfined --privileged \
-w $NFS_BASE \
-v /nfs:/nfs \
-v $USER_HOME:$USER_HOME \
$EXTRA_MOUNTS \
--shm-size 32G \
$DOCKER_IMAGE sleep infinity
docker ps --filter name=$CONTAINER --format 'table {{.Names}}\t{{.Status}}'" &
done
wait
echo "Docker started on ${#ALL_NODES[@]} node(s)"
Step 2 — Start Grafana and Prometheus
Grafana and Prometheus run on the decode node. Dashboards are served from the mori and SGLang source trees on NFS.
The Prometheus config is assembled locally so all N prefill targets can be folded into one scrape job, then piped over SSH to the decode node.
# Build the multi-prefill targets list as YAML (nested under `- targets:`).
PREFILL_TARGETS=""
for ip in "${PREFILL_IPS[@]}"; do
PREFILL_TARGETS+=" - '${ip}:30000'"$'\n'
done
cat > /tmp/pd_bench_prometheus.yml <<EOF
global:
scrape_interval: 5s
evaluation_interval: 30s
scrape_configs:
- job_name: sglang_prefill
static_configs:
- targets:
${PREFILL_TARGETS%$'\n'}
labels:
role: prefill
- job_name: sglang_decode
static_configs:
- targets: ['${IP_DECODE}:30001']
labels:
role: decode
- job_name: umbp_master
static_configs:
- targets: ['${IP_PREFILL_PRIMARY}:9091']
EOF
# Push to the decode node.
$SCP /tmp/pd_bench_prometheus.yml $NODE_DECODE:/tmp/pd_bench_prometheus.yml
$SSH $NODE_DECODE "
docker rm -f prometheus-pd 2>/dev/null || true
docker run -d --name prometheus-pd \
--network host \
-v /tmp/pd_bench_prometheus.yml:/etc/prometheus/prometheus.yml:ro \
prom/prometheus:latest \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/prometheus
docker rm -f grafana-pd 2>/dev/null || true
docker run -d --name grafana-pd \
--network host \
-v ${NFS_BASE}/sglang/examples/monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro \
-v ${NFS_BASE}/sglang/examples/monitoring/grafana/dashboards/config:/etc/grafana/provisioning/dashboards:ro \
-v ${NFS_BASE}/sglang/examples/monitoring/grafana/dashboards/json:/var/lib/grafana/dashboards:ro \
-v ${NFS_BASE}/mori/examples/monitoring/grafana/dashboards:/var/lib/grafana/mori_dashboards:ro \
-e GF_AUTH_ANONYMOUS_ENABLED=true \
-e GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer \
-e GF_AUTH_BASIC_ENABLED=false \
-e GF_USERS_ALLOW_SIGN_UP=false \
-e GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/sglang-dashboard.json \
grafana/grafana:latest
sleep 4
curl -sf http://localhost:9090/-/ready && echo 'Prometheus: READY' || echo 'Prometheus: NOT READY'
curl -sf http://localhost:3000/api/health | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Grafana:', 'HEALTHY' if d.get('database')=='ok' else 'NOT READY')\" 2>/dev/null || echo 'Grafana: NOT READY'
"
echo "=== Grafana: http://${IP_DECODE}:3000 ==="
echo "=== Prometheus: http://${IP_DECODE}:9090 ==="
Step 3 — Build mori on all nodes
Clears the per-node build directory so cmake starts clean, then builds with UMBP enabled. Builds run in parallel across every prefill node and the decode node.
for NODE in "${ALL_NODES[@]}"; do
$SSH $NODE "docker exec $CONTAINER bash -c '
rm -rf ${NFS_BASE}/mori/build_\$(hostname) &&
cd ${NFS_BASE}/mori && UMBP=ON bash build.sh'" &
done
wait
echo "mori builds done on ${#ALL_NODES[@]} node(s)"
Step 4 — Create launcher scripts
All launchers point at a single UMBP master on the primary prefill node
(${IP_PREFILL_PRIMARY}:15558). Per-node behavior differs only on three
variables:
Variable |
Primary prefill (i=0) |
Secondary prefill (i>=1) |
Decode |
|---|---|---|---|
|
(default true) |
|
|
|
|
|
|
|
|
|
|
UMBP_IO_ENGINE_PORT and UMBP_PEER_SERVICE_PORT are required whenever
UMBP_MASTER_ADDRESS is set. UMBP_NODE_ADDRESS must be unique per node so
all 8 * (N+1) dp ranks register distinct identities in the master.
Note on
USE_DUMMY_WEIGHTS: set totruebelow to skip loading real model weights, which speeds up startup and is useful for benchmarking transfer throughput. Set tofalse(or remove the variable) to run with actual weights.
# Helper that emits a prefill launcher for a given index.
# Index 0 is primary (auto-starts the UMBP master); indices >=1 connect to it.
emit_prefill_launcher() {
local idx="$1"
local node_ip="${PREFILL_IPS[$idx]}"
local out="/tmp/launch_pd_prefill_${idx}.sh"
local extra_master_line=""
if (( idx > 0 )); then
extra_master_line="export UMBP_MASTER_AUTO_START=false"
fi
cat > "$out" << LAUNCHEOF
#!/bin/bash
export PYTHONPATH=${NFS_BASE}/mori/python:/sgl-workspace/aiter
export MC_IB_TC=96
export MORI_ENABLE_SDMA=0
export SGLANG_DISAGGREGATION_WAITING_TIMEOUT=1800
export SGLANG_MORI_FP4_DISP=false
export SGLANG_MORI_FP8_DISP=true
export SGLANG_MORI_FP8_COMB=true
export SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK=2048
export NCCL_IB_HCA=${NCCL_IB_HCA}
export GLOO_SOCKET_IFNAME=${NET_IFNAME}
export NCCL_SOCKET_IFNAME=${NET_IFNAME}
export MORI_SOCKET_IFNAME=${NET_IFNAME}
export MORI_RDMA_DEVICES=${MORI_RDMA_DEVICES}
export SGLANG_USE_AITER=1
export KV_CACHE_DTYPE=fp8_e4m3
export UMBP_MASTER_ADDRESS=${IP_PREFILL_PRIMARY}:15558
${extra_master_line}
export UMBP_NODE_ADDRESS=${node_ip}
export UMBP_MASTER_BIN=${NFS_BASE}/mori/build_\$(hostname)/src/umbp/umbp_master
export UMBP_IO_ENGINE_HOST=127.0.0.1
export UMBP_IO_ENGINE_PORT=16000
export UMBP_PEER_SERVICE_PORT=16001
export UMBP_CACHE_REMOTE_FETCHES=false
export ENABLE_KV_EVENTS=true
export KV_EVENTS_PUBLISHER=zmq
export KV_EVENTS_ENDPOINT=tcp://*:5557
export KV_EVENTS_TOPIC=
export USE_DUMMY_WEIGHTS=true
export MEM_FRACTION_STATIC=0.7
export MORI_GLOBAL_LOG_LEVEL=info
export MORI_LOG_FILE=${USER_HOME}/mori_prefill_${idx}.log
exec bash ${NFS_BASE}/sglang/benchmark/hicache/run_pd_disagg_bench_dp8ep8.sh --role prefill
LAUNCHEOF
chmod +x "$out"
}
# Build space-separated PREFILL_URLS list for the decode launcher.
# The bench script's xP1D contract: pass each prefill URL once, separated by
# a space. Bootstrap port is shared (DISAGG_BOOTSTRAP_PORT, default 8998).
PREFILL_URLS_LIST=""
for ip in "${PREFILL_IPS[@]}"; do
PREFILL_URLS_LIST+="http://${ip}:30000 "
done
PREFILL_URLS_LIST="${PREFILL_URLS_LIST% }" # trim trailing space
cat > /tmp/launch_pd_decode.sh << LAUNCHEOF
#!/bin/bash
export PYTHONPATH=${NFS_BASE}/mori/python:/sgl-workspace/aiter
export MC_IB_TC=96
export MORI_ENABLE_SDMA=0
export SGLANG_DISAGGREGATION_WAITING_TIMEOUT=1800
export SGLANG_MORI_FP4_DISP=false
export SGLANG_MORI_FP8_DISP=true
export SGLANG_MORI_FP8_COMB=true
export SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK=2048
export NCCL_IB_HCA=${NCCL_IB_HCA}
export GLOO_SOCKET_IFNAME=${NET_IFNAME}
export NCCL_SOCKET_IFNAME=${NET_IFNAME}
export MORI_SOCKET_IFNAME=${NET_IFNAME}
export MORI_RDMA_DEVICES=${MORI_RDMA_DEVICES}
export SGLANG_USE_AITER=1
export KV_CACHE_DTYPE=fp8_e4m3
export UMBP_MASTER_ADDRESS=${IP_PREFILL_PRIMARY}:15558
export UMBP_MASTER_AUTO_START=false
export UMBP_NODE_ADDRESS=${IP_DECODE}
export UMBP_MASTER_BIN=${NFS_BASE}/mori/build_\$(hostname)/src/umbp/umbp_master
export UMBP_IO_ENGINE_HOST=127.0.0.1
export UMBP_IO_ENGINE_PORT=16000
export UMBP_PEER_SERVICE_PORT=16001
export UMBP_CACHE_REMOTE_FETCHES=false
export ENABLE_KV_EVENTS=true
export KV_EVENTS_PUBLISHER=zmq
export KV_EVENTS_ENDPOINT=tcp://*:5557
export KV_EVENTS_TOPIC=
export USE_DUMMY_WEIGHTS=true
export MEM_FRACTION_STATIC=0.7
export OUTPUT_LENGTH=${OUTPUT_LENGTH}
export PREFILL_URLS="${PREFILL_URLS_LIST}"
export MORI_GLOBAL_LOG_LEVEL=info
export MORI_LOG_FILE=${USER_HOME}/mori_decode.log
exec bash ${NFS_BASE}/sglang/benchmark/hicache/run_pd_disagg_bench_dp8ep8.sh --role decode
LAUNCHEOF
chmod +x /tmp/launch_pd_decode.sh
# Emit one prefill launcher per node and ship them.
for i in "${!PREFILL_NODES[@]}"; do
emit_prefill_launcher "$i"
$SCP "/tmp/launch_pd_prefill_${i}.sh" \
"${PREFILL_NODES[$i]}:${USER_HOME}/launch_pd_prefill.sh"
done
$SCP /tmp/launch_pd_decode.sh $NODE_DECODE:$USER_HOME/launch_pd_decode.sh
Step 5 — Kill stale processes and launch prefill
Clears any leftover SGLang or UMBP master processes from a previous run on every node, then launches prefill nodes. Startup order matters:
Primary first (
PREFILL_NODES[0]) — its bench script auto-starts the shared UMBP master.Secondaries (
PREFILL_NODES[1..N-1]) — only after the master is up, since they connect to it withUMBP_MASTER_AUTO_START=false.
For 1P1D, the secondaries loop is a no-op.
# Stale-process cleanup on all nodes (parallel).
for NODE in "${ALL_NODES[@]}"; do
$SSH $NODE 'docker exec umbp-pd-bench bash -c "
pkill -9 -f sglang 2>/dev/null
pkill -9 -f umbp_master 2>/dev/null
sleep 2
ss -tlnp | grep -E \":30000|:30001\" || echo prefill_decode_ports_free"' &
done
wait
# Marker file used by Step 6/8 to filter out previous-run log dirs on NFS.
RUN_START_MARKER="/tmp/pd_bench_run_start"
touch "$RUN_START_MARKER"
# 5a. Launch the PRIMARY prefill (auto-starts the UMBP master).
$SSH "$NODE_PREFILL_PRIMARY" "docker exec -d $CONTAINER bash -c \
'bash $USER_HOME/launch_pd_prefill.sh > $USER_HOME/pd_bench_prefill.log 2>&1'"
echo "Primary prefill launched on $NODE_PREFILL_PRIMARY"
# 5b. Wait for the UMBP master to be reachable, then launch secondaries.
# Secondaries fail fast if the master is not yet listening on :15558.
if (( N_PREFILLS > 1 )); then
echo "Waiting for UMBP master at ${IP_PREFILL_PRIMARY}:15558..."
for i in $(seq 1 60); do
if $SSH "$NODE_PREFILL_PRIMARY" "ss -tlnp 2>/dev/null | grep -q ':15558'"; then
echo " UMBP master is listening (took ${i}s window)."
break
fi
sleep 5
if (( i == 60 )); then
echo "ERROR: UMBP master did not start within 5min — abort." >&2
exit 1
fi
done
for i in $(seq 1 $((N_PREFILLS - 1))); do
node="${PREFILL_NODES[$i]}"
$SSH "$node" "docker exec -d $CONTAINER bash -c \
'bash $USER_HOME/launch_pd_prefill.sh > $USER_HOME/pd_bench_prefill.log 2>&1'"
echo "Secondary prefill launched on $node (idx $i)"
done
fi
Step 6 — Wait for ALL prefills ready
Logs are on NFS, so they can be read directly without SSH or docker exec.
For xP1D we wait until every prefill node’s server_prefill.log reports
fired up. Logs from previous runs are excluded via the RUN_START_MARKER
touched in Step 5.
The loop polls every 5 seconds and prints incremental output every 30 seconds.
sleep 10
declare -A LAST_LINES
for i in $(seq 1 180); do
# Discover all server_prefill.log files newer than RUN_START_MARKER.
mapfile -t LOGS < <(find "${RESULTS_BASE}/pd_disagg_prefill" \
-name server_prefill.log -newer "$RUN_START_MARKER" \
2>/dev/null | sort)
# Count READY logs.
READY_COUNT=0
for LOG in "${LOGS[@]}"; do
if grep -q "fired up" "$LOG" 2>/dev/null; then
READY_COUNT=$(( READY_COUNT + 1 ))
fi
done
if (( READY_COUNT >= N_PREFILLS )); then
echo "=== All ${N_PREFILLS} prefill(s) READY (iter $i) ==="
for LOG in "${LOGS[@]}"; do
echo "--- $LOG (last 3) ---"
tail -3 "$LOG"
done
break
fi
# Surface fatal errors from any prefill log.
ERR_HIT=false
for LOG in "${LOGS[@]}"; do
if grep -qE "^Traceback \(most recent|^[[:space:]]*(RuntimeError|AssertionError|ImportError):|CUDA error:|out of memory|Segmentation fault|core dumped|^Killed" "$LOG" 2>/dev/null; then
echo "=== ERROR in $LOG (iter $i) ==="
tail -60 "$LOG"
ERR_HIT=true
break
fi
done
$ERR_HIT && break
# Periodic incremental tail per log.
if (( i % 6 == 0 )); then
echo "--- iter $i: ${READY_COUNT}/${N_PREFILLS} prefill(s) ready ---"
for LOG in "${LOGS[@]}"; do
TOTAL=$(wc -l < "$LOG" 2>/dev/null || echo 0)
LAST="${LAST_LINES[$LOG]:-0}"
if (( TOTAL > LAST )); then
echo " >>> $LOG (lines $LAST -> $TOTAL)"
tail -n +"$((LAST + 1))" "$LOG" | head -10
LAST_LINES[$LOG]=$TOTAL
fi
done
fi
# Wrapper logs on every prefill node — surface FATAL/FAILED early.
WRAP_HIT=false
for NODE in "${PREFILL_NODES[@]}"; do
WLOG=$($SSH "$NODE" "cat $USER_HOME/pd_bench_prefill.log 2>/dev/null" || true)
if echo "$WLOG" | grep -qiE "^\[.*\] FATAL|^\[.*\] FAILED"; then
echo "=== FATAL in wrapper log on $NODE ==="
echo "$WLOG" | tail -20
WRAP_HIT=true
break
fi
done
$WRAP_HIT && break
sleep 5
done
Step 7 — Launch decode and benchmark
Decode connects to the already-running UMBP master on the primary prefill
node, and the bench script’s sglang_router fans out to all N prefills via
PREFILL_URLS. The wrapper waits for all N+1 workers to register on the
router before starting the bench (timeout: ROUTER_WORKER_READY_TIMEOUT_SECS,
default 180s).
$SSH $NODE_DECODE "docker exec -d $CONTAINER bash -c \
'bash $USER_HOME/launch_pd_decode.sh > $USER_HOME/pd_bench_decode.log 2>&1'"
echo "Decode + benchmark launched (router will fan out to ${N_PREFILLS} prefill(s))"
Step 8 — Monitor decode and benchmark
Polls every 15 seconds. Prints incremental server log every 60 seconds. Exits when the benchmark reports completion or a fatal error is detected.
sleep 10
DECODE_READY=false
LAST=0
for i in $(seq 1 720); do
# Filter out previous-run decode logs via RUN_START_MARKER from Step 5.
LOG=$(find ${RESULTS_BASE}/pd_disagg_decode -name server_decode.log \
-newer "$RUN_START_MARKER" 2>/dev/null | sort -r | head -1)
if [[ -n "$LOG" ]]; then
if ! $DECODE_READY && grep -q "fired up" "$LOG"; then
echo "--- Decode READY (iter $i), benchmark starting ---"
DECODE_READY=true
fi
if grep -qE "^Traceback \(most recent|^[[:space:]]*(RuntimeError|AssertionError|ImportError):|CUDA error:|out of memory|Segmentation fault|core dumped|^Killed" "$LOG"; then
echo "=== ERROR in decode log (iter $i) ===" && tail -60 "$LOG" && break
fi
TOTAL=$(wc -l < "$LOG")
if (( i % 4 == 0 && TOTAL > LAST )); then
echo "--- decode log update (iter $i, lines $LAST→$TOTAL) ---"
tail -n +"$((LAST + 1))" "$LOG" | head -15
LAST=$TOTAL
fi
fi
WLOG=$($SSH $NODE_DECODE "cat $USER_HOME/pd_bench_decode.log 2>/dev/null" || true)
if echo "$WLOG" | grep -q "Benchmark finished in"; then
echo "=== Benchmark COMPLETE (iter $i) ===" && echo "$WLOG" | tail -20 && break
fi
if echo "$WLOG" | grep -qiE "^\[.*\] FATAL|SERVER_CRASH|^\[.*\] FAILED"; then
echo "=== FATAL in decode wrapper (iter $i) ===" && echo "$WLOG" | tail -30
[[ -n "$LOG" ]] && tail -30 "$LOG"
break
fi
sleep 15
done
Step 9 — Show results
For xP1D, all N prefill logs are listed (one per node). The
RUN_START_MARKER filter ensures we only see this run’s logs.
echo "=== Prefill server logs (this run, last 20 each) ==="
mapfile -t PLOGS < <(find ${RESULTS_BASE}/pd_disagg_prefill \
-name server_prefill.log -newer "$RUN_START_MARKER" \
2>/dev/null | sort)
for LOG in "${PLOGS[@]}"; do
echo "--- $LOG ---"
tail -20 "$LOG"
done
echo "=== Decode server log (last 20) ==="
find ${RESULTS_BASE}/pd_disagg_decode -name server_decode.log \
-newer "$RUN_START_MARKER" 2>/dev/null | sort -r | head -1 | xargs tail -20
echo "=== Decode wrapper log (last 20) ==="
$SSH $NODE_DECODE "tail -20 $USER_HOME/pd_bench_decode.log"
echo "=== Summary ==="
find ${RESULTS_BASE}/pd_disagg_decode -name summary.txt \
-newer "$RUN_START_MARKER" 2>/dev/null | sort -r | head -1 | xargs cat 2>/dev/null
echo "=== Metrics ==="
find ${RESULTS_BASE}/pd_disagg_decode -name performance_metrics.jsonl \
-newer "$RUN_START_MARKER" 2>/dev/null | sort -r | head -1 | xargs tail -5 2>/dev/null
Teardown
for NODE in "${ALL_NODES[@]}"; do
$SSH $NODE "
docker exec $CONTAINER bash -c 'pkill -9 -f sglang; pkill -9 -f umbp_master' 2>/dev/null || true
docker rm -f $CONTAINER" &
done
$SSH $NODE_DECODE "docker rm -f prometheus-pd grafana-pd 2>/dev/null || true" &
wait
Environment Variable Reference
Topology (this guide’s wrapper variables)
Variable |
Description |
|---|---|
|
Bash array of prefill hostnames; index 0 is the primary (hosts the UMBP master). Length determines N in xP1D. |
|
Bash array of prefill IPs; same length and order as |
|
Derived: |
|
Derived: |
|
Derived: |
|
The single decode node. |
|
Derived: |
UMBP / mori (consumed by run_pd_disagg_bench_dp8ep8.sh)
Variable |
Description |
|---|---|
|
|
|
|
|
This node’s IP — must be unique per node so all |
|
Path to the |
|
Host for the local IO engine listener (always |
|
Port for the local IO engine; required when |
|
Port for peer-to-peer service; required when |
|
Set |
|
Enable FP8 dispatch (reduces transfer size) |
|
Enable FP8 combine |
|
RDMA devices to use; prefix |
|
Network interface for mori’s TCP socket |
|
KV cache dtype; |
|
KV event publisher backend ( |
|
ZMQ endpoint for KV events (e.g., |
|
Enable KV event publishing |
|
Skip loading real model weights (for throughput benchmarking) |
|
Fraction of GPU memory reserved for static KV cache |
|
Number of decode output tokens per request (decode node only) |
|
Space-separated prefill URLs for the decode node’s |
|
[DEPRECATED] Single-prefill shortcut (1P1D back-compat); folded into |
|
Max seconds the wrapper waits for all |
Troubleshooting
SSH access denied on prefill node from local machine
Use the decode node as a jumphost to authorize your key on every prefill node.
PUBKEY=$(cat $USER_HOME/.ssh/id_ed25519.pub)
for NODE in "${PREFILL_NODES[@]}"; do
$SSH $NODE_DECODE "ssh -o StrictHostKeyChecking=no $NODE \
\"mkdir -p ~/.ssh && echo '$PUBKEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys\""
done
Server fails to start / ImportError
Check PYTHONPATH includes both the mori Python bindings and aiter:
export PYTHONPATH=${NFS_BASE}/mori/python:/sgl-workspace/aiter
The NFS copy of mori (${NFS_BASE}/mori/python) is authoritative. The
docker-internal /sgl-workspace/mori may be an older version and should not
be used.
Errors appear in the wrong log
Always check the actual SGLang server log, not the wrapper log:
# Prefill
find ${RESULTS_BASE}/pd_disagg_prefill -name server_prefill.log | sort -r | head -1 | xargs tail -50
# Decode
find ${RESULTS_BASE}/pd_disagg_decode -name server_decode.log | sort -r | head -1 | xargs tail -50
Port already in use
Kill stale processes from a previous run (see Step 5) and verify the port is free.
For xP1D, only the primary prefill is expected to be listening on :15558 (UMBP master);
secondary prefills should not be (they connect to the primary).
for NODE in "${PREFILL_NODES[@]}"; do
echo "--- $NODE ---"
$SSH $NODE "ss -tlnp | grep -E ':30000|:15558|:16000|:16001' || echo all_free"
done
echo "--- $NODE_DECODE ---"
$SSH $NODE_DECODE "ss -tlnp | grep -E ':30001|:16000|:16001|:8000' || echo all_free"
Bench progress freezes partway
If bench_multiturn sits at e.g. 4/8 for minutes with no new requests,
check the decode wrapper log for WARNING: router workers not all healthy.
That means ROUTER_WORKER_READY_TIMEOUT_SECS (default 180s) elapsed before
all N+1 workers registered, and the bench then raced a 503 "No decode workers available". Increase the timeout and re-run:
$SSH $NODE_DECODE "docker exec $CONTAINER bash -c \
'ROUTER_WORKER_READY_TIMEOUT_SECS=300 bash $USER_HOME/launch_pd_decode.sh > $USER_HOME/pd_bench_decode.log 2>&1'"