Skip to content

Commit 89696c5

Browse files
ichbinblauinkcherrylixiufei-leocursoragentchunfangamd
authored
[AMD][dsv4] Fix 2P1D eval 503s/straggler tail, retune disagg concurrency sweep (#2293)
* [AMD][dsv4] fix 2P1D eval 503s and 8k1k straggler tail - Speed circuit-breaker recovery and gate benchmarking on an end-to-end router readiness canary, so a transient trip or a not-yet-serving router doesn't 503 every request until the client's retry budget is exhausted (port of PR #2255). - Keep decode's admission context-length symmetric with prefill (falling back to prefill's value when unset in models.yaml), so over-length requests are rejected fast by both sides instead of hanging on decode while waiting for a KV transfer prefill never sends (port of PR #2257). Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: inkcherry <mingzhi.liu@amd.com> * add con=500/512 Signed-off-by: Theresa Shan <theresa.shan@amd.com> * set max_total_tokens: 2097152 Signed-off-by: Theresa Shan <theresa.shan@amd.com> Co-authored-by: lixiufei-leo <Xiufei.Li@amd.com> * update config Signed-off-by: Theresa Shan <theresa.shan@amd.com> * [AMD][dsv4] Replace 2P1D DEP8 sweep with 1P1D pure-TP8 in disagg non-MTP search space Drop the 2P1D DEP8 (conc 256/512/1024) and 1P1D DEP8 conc=1024 entries and add a 1P1D pure-TP8 (no EP, no DP-attention) scenario sweeping conc [1, 2, 4, 8, 16, 32, 64, 128] for dsv4-fp4-mi355x-sglang-disagg. Co-authored-by: Cursor <cursoragent@cursor.com> * remove decode context_length Squash of f7295a5 "remove decode context_length", 0be1d30 "Revert 'remove decode context_length' for DeepSeek-V4-Pro", and bfb8988 "remove decode context_length ending with a newline" into a single commit with the same net result. Signed-off-by: Theresa Shan <theresa.shan@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Add perf-changelog.yaml entry for dsv4-fp4-mi355x-sglang-disagg retune Per the automated PR reviewer: any edit to configs/amd-master.yaml requires a paired perf-changelog.yaml entry. Documents the 503/ straggler-tail fixes, the max_total_tokens bump, and the 2P1D DEP8 -> 1P1D DEP8/pure-TP8 concurrency sweep retune from PR #2293. Co-authored-by: Cursor <cursoragent@cursor.com> * Guard router readiness canary with wait_or_die on prefill0_pid The canary poll loop was a plain while/curl loop, unlike every other blocking wait on node rank 0 (container barrier, servers-up barrier, /readiness health barrier), so a prefill crash right after /readiness passed just looked like repeated 503s and burned the full ROUTER_CANARY_TIMEOUT (default 600s) instead of failing fast. Extract the loop into run_router_canary() and run it under wait_or_die so a dead prefill pid aborts it in ~5s like the surrounding barriers. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Theresa Shan <theresa.shan@amd.com> Co-authored-by: inkcherry <mingzhi.liu@amd.com> Co-authored-by: lixiufei-leo <Xiufei.Li@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Chun Fang <chun.fang@amd.com>
1 parent a4bb43a commit 89696c5

4 files changed

Lines changed: 123 additions & 26 deletions

File tree

benchmarks/multi_node/amd_utils/models.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,12 @@ DeepSeek-V4-Pro-DI:
409409
max_running_requests: 1024
410410
chunked_prefill_size: 131072
411411
context_length: 9217
412-
max_total_tokens: 262144
412+
max_total_tokens: 2097152
413413
no_dp:
414414
max_running_requests: 128
415415
chunked_prefill_size: 131072
416416
context_length: 9217
417-
max_total_tokens: 262144
417+
max_total_tokens: 2097152
418418
decode:
419419
mem_fraction_static: 0.85
420420
prefill_round_robin_balance: true

benchmarks/multi_node/amd_utils/server_sglang.sh

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,23 @@ no_dp = decode.get('no_dp', {})
165165
# Decode DP config
166166
print(f'DECODE_MAX_RUNNING_REQUESTS_DP=\"{dp.get(\"max_running_requests\", 4096)}\"')
167167
print(f'DECODE_CHUNKED_PREFILL_SIZE_DP=\"{eval_formula(dp.get(\"chunked_prefill_size\", 262144))}\"')
168+
print(f'DECODE_CONTEXT_LENGTH_DP=\"{dp.get(\"context_length\", \"\")}\"')
168169
s, e = parse_range(dp.get('cuda_graph_bs_range', '1-160'), 1, 160)
169170
print(f'DECODE_CUDA_GRAPH_BS_DP_START=\"{s}\"')
170171
print(f'DECODE_CUDA_GRAPH_BS_DP_END=\"{e}\"')
171172
172173
# Decode EP-only config (EP enabled but DP disabled)
173174
print(f'DECODE_MAX_RUNNING_REQUESTS_EP_ONLY=\"{ep_only.get(\"max_running_requests\", 256)}\"')
174175
print(f'DECODE_CHUNKED_PREFILL_SIZE_EP_ONLY=\"{eval_formula(ep_only.get(\"chunked_prefill_size\", 262144))}\"')
176+
print(f'DECODE_CONTEXT_LENGTH_EP_ONLY=\"{ep_only.get(\"context_length\", \"\")}\"')
175177
s, e = parse_range(ep_only.get('cuda_graph_bs_range', '1-256'), 1, 256)
176178
print(f'DECODE_CUDA_GRAPH_BS_EP_ONLY_START=\"{s}\"')
177179
print(f'DECODE_CUDA_GRAPH_BS_EP_ONLY_END=\"{e}\"')
178180
179181
# Decode no-DP config
180182
print(f'DECODE_MAX_RUNNING_REQUESTS_NO_DP=\"{no_dp.get(\"max_running_requests\", 128)}\"')
181183
print(f'DECODE_CHUNKED_PREFILL_SIZE_NO_DP=\"{eval_formula(no_dp.get(\"chunked_prefill_size\", 262144))}\"')
184+
print(f'DECODE_CONTEXT_LENGTH_NO_DP=\"{no_dp.get(\"context_length\", \"\")}\"')
182185
s, e = parse_range(no_dp.get('cuda_graph_bs_range', '1-128'), 1, 128)
183186
print(f'DECODE_CUDA_GRAPH_BS_NO_DP_START=\"{s}\"')
184187
print(f'DECODE_CUDA_GRAPH_BS_NO_DP_END=\"{e}\"')
@@ -216,12 +219,23 @@ fi
216219
if [[ "$DECODE_ENABLE_DP" == "true" ]]; then
217220
decode_cuda_graph_bs=($(seq $DECODE_CUDA_GRAPH_BS_DP_START $DECODE_CUDA_GRAPH_BS_DP_END))
218221
decode_max_running_requests=$((DECODE_CUDA_GRAPH_BS_DP_END * DECODE_TP_SIZE))
222+
decode_context_length=$DECODE_CONTEXT_LENGTH_DP
219223
elif [[ "$DECODE_ENABLE_EP" == "true" ]]; then
220224
decode_cuda_graph_bs=($(seq $DECODE_CUDA_GRAPH_BS_EP_ONLY_START $DECODE_CUDA_GRAPH_BS_EP_ONLY_END))
221225
decode_max_running_requests=$DECODE_MAX_RUNNING_REQUESTS_EP_ONLY
226+
decode_context_length=$DECODE_CONTEXT_LENGTH_EP_ONLY
222227
else
223228
decode_cuda_graph_bs=($(seq $DECODE_CUDA_GRAPH_BS_NO_DP_START $DECODE_CUDA_GRAPH_BS_NO_DP_END))
224229
decode_max_running_requests=$DECODE_MAX_RUNNING_REQUESTS_NO_DP
230+
decode_context_length=$DECODE_CONTEXT_LENGTH_NO_DP
231+
fi
232+
# In PD-disaggregation the decode must admit requests against the SAME context
233+
# length as prefill; otherwise decode accepts over-length requests that prefill
234+
# rejects, and those requests hang forever waiting for a KV transfer that never
235+
# comes (the 8k1k conc-500 straggler). Fall back to the prefill value if the
236+
# decode context_length is not set in the model config, so the two always agree.
237+
if [[ -z "$decode_context_length" ]]; then
238+
decode_context_length=$prefill_context_length
225239
fi
226240

227241
# When both DP and EP are enabled, override max-running-requests and dispatch tokens
@@ -267,6 +281,10 @@ DECODE_MODE_FLAGS="--mem-fraction-static ${DECODE_MEM_FRACTION_STATIC} --max-run
267281
if [[ "$DECODE_PREFILL_ROUND_ROBIN_BALANCE" == "True" ]] || [[ "$DECODE_PREFILL_ROUND_ROBIN_BALANCE" == "true" ]]; then
268282
DECODE_MODE_FLAGS="$DECODE_MODE_FLAGS --prefill-round-robin-balance"
269283
fi
284+
if [[ -n "$decode_context_length" ]]; then
285+
DECODE_MODE_FLAGS="$DECODE_MODE_FLAGS --context-length ${decode_context_length}"
286+
fi
287+
270288
if [[ "$DECODE_DISAGG_ENABLE_RADIX_CACHE" == "True" ]] || [[ "$DECODE_DISAGG_ENABLE_RADIX_CACHE" == "true" ]]; then
271289
DECODE_MODE_FLAGS="$DECODE_MODE_FLAGS --disaggregation-decode-enable-radix-cache"
272290
fi
@@ -750,26 +768,37 @@ if [ "$NODE_RANK" -eq 0 ]; then
750768
fi
751769
echo "Congratulations!!! All prefill and decode servers are up . . ."
752770

753-
# Router resilience: a single prefill worker doing huge long-context prefills
754-
# (256K+ token agentic prompts in 65280-token chunks) can be slow to drain a
755-
# concurrent burst. With defaults the circuit breaker opens after 10 failures
756-
# and short-circuits the whole worker, so the aiperf profiling burst sees
757-
# "No available prefill workers (all circuits open or unhealthy)" and aborts
758-
# with 100% errors. Disable the breaker and relax health-check sensitivity so
759-
# a busy-but-alive worker is not ejected. Override via ROUTER_RESILIENCE_FLAGS.
760-
ROUTER_RESILIENCE_FLAGS="${ROUTER_RESILIENCE_FLAGS:---disable-circuit-breaker --health-failure-threshold 100 --health-check-timeout-secs 600 --health-check-interval-secs 30}"
761-
762-
# Router scheduling policy. cache_aware prefill routing exploits HiCache/radix
763-
# prefix reuse across the agentic trace; round_robin decode keeps the single
764-
# decode worker fed evenly. cache_threshold / balance_*_threshold tune the
765-
# cache_aware load-balancing (router defaults are 0.5 / 64 / 1.5). Override any
766-
# of these via env.
767-
ROUTER_PREFILL_POLICY="${ROUTER_PREFILL_POLICY:-cache_aware}"
768-
ROUTER_DECODE_POLICY="${ROUTER_DECODE_POLICY:-round_robin}"
769-
ROUTER_CACHE_THRESHOLD="${ROUTER_CACHE_THRESHOLD:-0.3}"
770-
ROUTER_BALANCE_ABS_THRESHOLD="${ROUTER_BALANCE_ABS_THRESHOLD:-2}"
771-
ROUTER_BALANCE_REL_THRESHOLD="${ROUTER_BALANCE_REL_THRESHOLD:-1.1}"
772-
ROUTER_POLICY_FLAGS="${ROUTER_POLICY_FLAGS:---policy ${ROUTER_PREFILL_POLICY} --prefill-policy ${ROUTER_PREFILL_POLICY} --decode-policy ${ROUTER_DECODE_POLICY} --cache-threshold ${ROUTER_CACHE_THRESHOLD} --balance-abs-threshold ${ROUTER_BALANCE_ABS_THRESHOLD} --balance-rel-threshold ${ROUTER_BALANCE_REL_THRESHOLD}}"
771+
if [[ "${IS_AGENTIC:-0}" == "1" || "${IS_AGENTIC:-}" == "true" ]]; then
772+
# Agentic router config (main): long-context prefills can look unhealthy to
773+
# the default circuit breaker during a concurrent burst. Disable the breaker
774+
# and relax health-check sensitivity so a busy-but-alive worker is not
775+
# ejected. cache_aware prefill routing exploits HiCache/radix prefix reuse
776+
# across the agentic trace; round_robin decode keeps the single decode worker
777+
# fed evenly. Override via ROUTER_RESILIENCE_FLAGS / ROUTER_POLICY_FLAGS.
778+
ROUTER_RESILIENCE_FLAGS="${ROUTER_RESILIENCE_FLAGS:---disable-circuit-breaker --health-failure-threshold 100 --health-check-timeout-secs 600 --health-check-interval-secs 30}"
779+
ROUTER_PREFILL_POLICY="${ROUTER_PREFILL_POLICY:-cache_aware}"
780+
ROUTER_DECODE_POLICY="${ROUTER_DECODE_POLICY:-round_robin}"
781+
ROUTER_CACHE_THRESHOLD="${ROUTER_CACHE_THRESHOLD:-0.3}"
782+
ROUTER_BALANCE_ABS_THRESHOLD="${ROUTER_BALANCE_ABS_THRESHOLD:-2}"
783+
ROUTER_BALANCE_REL_THRESHOLD="${ROUTER_BALANCE_REL_THRESHOLD:-1.1}"
784+
ROUTER_POLICY_FLAGS="${ROUTER_POLICY_FLAGS:---policy ${ROUTER_PREFILL_POLICY} --prefill-policy ${ROUTER_PREFILL_POLICY} --decode-policy ${ROUTER_DECODE_POLICY} --cache-threshold ${ROUTER_CACHE_THRESHOLD} --balance-abs-threshold ${ROUTER_BALANCE_ABS_THRESHOLD} --balance-rel-threshold ${ROUTER_BALANCE_REL_THRESHOLD}}"
785+
else
786+
# DI router config (8k1k branch, run 28696443568): with defaults the per-worker
787+
# circuit stays OPEN for cb-timeout-duration-secs=60 before a half-open retrial.
788+
# In that run every request 503'd from request #1 ("all circuits open or
789+
# unhealthy") for ~31s and lm_eval (max_retries=5) then gave up -- i.e. the
790+
# circuit was still open when the client budget ran out, so 0 result files were
791+
# produced. Shortening the open->half-open window (and letting the router itself
792+
# retry a failed worker selection) lets a transient trip re-close INSIDE the
793+
# client retry budget instead of nuking the whole eval. The breaker stays fully
794+
# ENABLED (thresholds unchanged); this only speeds recovery. Override via
795+
# ROUTER_CB_ARGS / ROUTER_POLICY_FLAGS.
796+
ROUTER_CB_ARGS="${ROUTER_CB_ARGS:---cb-timeout-duration-secs 15 --retry-max-retries 3}"
797+
ROUTER_POLICY_FLAGS="${ROUTER_POLICY_FLAGS:---policy random --prefill-policy random --decode-policy random}"
798+
ROUTER_RESILIENCE_FLAGS="${ROUTER_RESILIENCE_FLAGS:-${ROUTER_CB_ARGS}}"
799+
fi
800+
801+
echo "Router config: IS_AGENTIC=${IS_AGENTIC:-0} policy/resilience=${ROUTER_POLICY_FLAGS} ${ROUTER_RESILIENCE_FLAGS}"
773802

774803
ROUTER_CMD="python -m sglang_router.launch_router \
775804
--pd-disaggregation \
@@ -822,6 +851,47 @@ if [ "$NODE_RANK" -eq 0 ]; then
822851
wait_or_die "$prefill0_pid" bash -c "$HEALTH_BARRIER_CMD" || exit 1
823852
fi
824853

854+
# ---- End-to-end router readiness canary (run 28696443568) ----
855+
# The /readiness barrier above only proves the router PROCESS is up; it does
856+
# NOT prove the router can reach a prefill worker and complete a generation.
857+
# In that run the eval fired the instant /readiness passed and EVERY request
858+
# 503'd ("No available prefill workers (all circuits open or unhealthy)") from
859+
# request #1 -> lm_eval gave up -> 0 result files -> "Verify eval scores" failed.
860+
# Gate the benchmark on ONE successful generation THROUGH the router so the eval
861+
# never starts against a router whose prefill path is not yet actually serving.
862+
#
863+
# Run the poll loop under wait_or_die (like the barriers above) rather than
864+
# inline: the canary is the FIRST real generation through prefill, so a
865+
# prefill crash right after /readiness passes is plausible. Without
866+
# wait_or_die watching prefill0_pid, that just looks like repeated 503s and
867+
# the loop burns the full ROUTER_CANARY_TIMEOUT (default 600s) instead of
868+
# detecting the dead pid and aborting in ~5s.
869+
run_router_canary() {
870+
local canary_url="http://${NODE0_ADDR}:30000/v1/chat/completions"
871+
local canary_model="${MODEL_DIR}/${MODEL_NAME}"
872+
local canary_deadline=$(( $(date +%s) + ${ROUTER_CANARY_TIMEOUT:-600} ))
873+
local canary_code
874+
while [ "$(date +%s)" -lt "$canary_deadline" ]; do
875+
canary_code=$(curl -s -o /tmp/router_canary.out -w '%{http_code}' \
876+
-m "${ROUTER_CANARY_REQ_TIMEOUT:-120}" \
877+
-X POST "$canary_url" -H 'Content-Type: application/json' \
878+
-d "{\"model\":\"${canary_model}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"temperature\":0}" 2>/dev/null)
879+
if [ "$canary_code" = "200" ] && \
880+
! grep -qE "circuits open|server_selection_failed|No available" /tmp/router_canary.out 2>/dev/null; then
881+
echo "Router readiness canary passed (end-to-end generation OK)"
882+
return 0
883+
fi
884+
echo "Router readiness canary not ready yet (http=${canary_code}); retrying in 5s . . ."
885+
sleep 5
886+
done
887+
echo "ERROR: router readiness canary failed after ${ROUTER_CANARY_TIMEOUT:-600}s -- the router cannot complete a generation through a prefill worker (all circuits open/unhealthy). Refusing to start the eval against a non-serving router."
888+
head -c 800 /tmp/router_canary.out 2>/dev/null
889+
return 1
890+
}
891+
if [[ "${ROUTER_READINESS_CANARY:-1}" == "1" ]]; then
892+
wait_or_die "$prefill0_pid" run_router_canary || exit 1
893+
fi
894+
825895
echo "Router is ready for benchmarking"
826896
fi
827897

configs/amd-master.yaml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,17 +1166,34 @@ dsv4-fp4-mi355x-sglang-disagg:
11661166
additional-settings:
11671167
- "DECODE_NODES=1"
11681168
- "DECODE_MTP_SIZE=0"
1169-
11701169
# 1P1D DEP8 (mori KV transfer + mori MoE a2a, dp-attention)
11711170
- spec-decoding: "none"
1172-
conc-list: [ 128, 256 ]
1171+
conc-list: [ 256 ]
11731172
prefill:
1174-
num-worker: 2
1173+
num-worker: 1
11751174
tp: 8
11761175
ep: 8
11771176
dp-attn: true
11781177
additional-settings:
1179-
- "PREFILL_NODES=2"
1178+
- "PREFILL_NODES=1"
1179+
decode:
1180+
num-worker: 1
1181+
tp: 8
1182+
ep: 8
1183+
dp-attn: true
1184+
additional-settings:
1185+
- "DECODE_NODES=1"
1186+
- "DECODE_MTP_SIZE=0"
1187+
1188+
- spec-decoding: "none"
1189+
conc-list: [ 512 ]
1190+
prefill:
1191+
num-worker: 1
1192+
tp: 8
1193+
ep: 8
1194+
dp-attn: true
1195+
additional-settings:
1196+
- "PREFILL_NODES=1"
11801197
decode:
11811198
num-worker: 1
11821199
tp: 8

perf-changelog.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4983,6 +4983,16 @@
49834983
- "Low-latency TP8 arm (conc [1, 2, 4, 8, 16, 32], fp8 KV + cutedsl bf16 GEMM) unchanged; DEP mirrors the proven dsv4-fp4-b300-sglang tp8/ep8/dp-attn DEP arm"
49844984
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2281
49854985

4986+
- config-keys:
4987+
- dsv4-fp4-mi355x-sglang-disagg
4988+
description:
4989+
- "Retune the non-MTP 8k1k disagg search space: drop the 2P1D DEP8 sweep (conc 128/256/512/1024 across 2 prefill nodes) and the 1P1D DEP8 conc=1024 point"
4990+
- "Add 1P1D DEP8 (TP8/EP8 + dp-attention, 1 prefill node) at conc 256/512, and a new 1P1D pure-TP8 (no EP, no DP-attention) scenario sweeping conc [1, 2, 4, 8, 16, 32, 64, 128]"
4991+
- "Fix 2P1D eval 503s: speed up router circuit-breaker recovery (--cb-timeout-duration-secs 15 --retry-max-retries 3) and gate benchmarking on an end-to-end router readiness canary (one real generation through the router), so the eval no longer starts against a router whose prefill path isn't actually serving yet"
4992+
- "Fix the 8k1k conc-500 straggler tail: decode now derives its admission context_length from models.yaml (falling back to prefill's value when unset) so decode and prefill reject over-length requests symmetrically instead of decode hanging on a KV transfer prefill never sends"
4993+
- "Bump DeepSeek-V4-Pro prefill max_total_tokens from 262144 to 2097152 to match the model's actual KV budget"
4994+
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2293
4995+
49864996
- config-keys:
49874997
- dsv4-fp4-b300-vllm-agentic
49884998
scenario-type:

0 commit comments

Comments
 (0)