Skip to content

Commit 4ac75e3

Browse files
ichbinblauinkcherry
andcommitted
[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>
1 parent 673a601 commit 4ac75e3

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

benchmarks/multi_node/amd_utils/models.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,15 @@ DeepSeek-V4-Pro:
395395
dp:
396396
max_running_requests: 1024
397397
cuda_graph_bs_range: "1-128"
398+
# Must match prefill.context_length. In PD-disagg the decode admits requests
399+
# against its own context length; if left unset it defaults to the model's
400+
# (huge) config value, so a request whose input exceeds prefill's 9217 budget
401+
# is accepted by decode (pre-allocated, waiting for KV) but rejected by prefill
402+
# (never sends KV) -> the request hangs to the client timeout (the 8k1k
403+
# conc-500 straggler tail). Keeping decode symmetric with prefill makes such
404+
# over-length requests fail fast at admission instead.
405+
context_length: 9217
398406
no_dp:
399407
max_running_requests: 128
400408
cuda_graph_bs_range: "1-128"
409+
context_length: 9217

benchmarks/multi_node/amd_utils/server_sglang.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,23 @@ no_dp = decode.get('no_dp', {})
154154
# Decode DP config
155155
print(f'DECODE_MAX_RUNNING_REQUESTS_DP=\"{dp.get(\"max_running_requests\", 4096)}\"')
156156
print(f'DECODE_CHUNKED_PREFILL_SIZE_DP=\"{eval_formula(dp.get(\"chunked_prefill_size\", 262144))}\"')
157+
print(f'DECODE_CONTEXT_LENGTH_DP=\"{dp.get(\"context_length\", \"\")}\"')
157158
s, e = parse_range(dp.get('cuda_graph_bs_range', '1-160'), 1, 160)
158159
print(f'DECODE_CUDA_GRAPH_BS_DP_START=\"{s}\"')
159160
print(f'DECODE_CUDA_GRAPH_BS_DP_END=\"{e}\"')
160161
161162
# Decode EP-only config (EP enabled but DP disabled)
162163
print(f'DECODE_MAX_RUNNING_REQUESTS_EP_ONLY=\"{ep_only.get(\"max_running_requests\", 256)}\"')
163164
print(f'DECODE_CHUNKED_PREFILL_SIZE_EP_ONLY=\"{eval_formula(ep_only.get(\"chunked_prefill_size\", 262144))}\"')
165+
print(f'DECODE_CONTEXT_LENGTH_EP_ONLY=\"{ep_only.get(\"context_length\", \"\")}\"')
164166
s, e = parse_range(ep_only.get('cuda_graph_bs_range', '1-256'), 1, 256)
165167
print(f'DECODE_CUDA_GRAPH_BS_EP_ONLY_START=\"{s}\"')
166168
print(f'DECODE_CUDA_GRAPH_BS_EP_ONLY_END=\"{e}\"')
167169
168170
# Decode no-DP config
169171
print(f'DECODE_MAX_RUNNING_REQUESTS_NO_DP=\"{no_dp.get(\"max_running_requests\", 128)}\"')
170172
print(f'DECODE_CHUNKED_PREFILL_SIZE_NO_DP=\"{eval_formula(no_dp.get(\"chunked_prefill_size\", 262144))}\"')
173+
print(f'DECODE_CONTEXT_LENGTH_NO_DP=\"{no_dp.get(\"context_length\", \"\")}\"')
171174
s, e = parse_range(no_dp.get('cuda_graph_bs_range', '1-128'), 1, 128)
172175
print(f'DECODE_CUDA_GRAPH_BS_NO_DP_START=\"{s}\"')
173176
print(f'DECODE_CUDA_GRAPH_BS_NO_DP_END=\"{e}\"')
@@ -205,12 +208,23 @@ fi
205208
if [[ "$DECODE_ENABLE_DP" == "true" ]]; then
206209
decode_cuda_graph_bs=($(seq $DECODE_CUDA_GRAPH_BS_DP_START $DECODE_CUDA_GRAPH_BS_DP_END))
207210
decode_max_running_requests=$((DECODE_CUDA_GRAPH_BS_DP_END * DECODE_TP_SIZE))
211+
decode_context_length=$DECODE_CONTEXT_LENGTH_DP
208212
elif [[ "$DECODE_ENABLE_EP" == "true" ]]; then
209213
decode_cuda_graph_bs=($(seq $DECODE_CUDA_GRAPH_BS_EP_ONLY_START $DECODE_CUDA_GRAPH_BS_EP_ONLY_END))
210214
decode_max_running_requests=$DECODE_MAX_RUNNING_REQUESTS_EP_ONLY
215+
decode_context_length=$DECODE_CONTEXT_LENGTH_EP_ONLY
211216
else
212217
decode_cuda_graph_bs=($(seq $DECODE_CUDA_GRAPH_BS_NO_DP_START $DECODE_CUDA_GRAPH_BS_NO_DP_END))
213218
decode_max_running_requests=$DECODE_MAX_RUNNING_REQUESTS_NO_DP
219+
decode_context_length=$DECODE_CONTEXT_LENGTH_NO_DP
220+
fi
221+
# In PD-disaggregation the decode must admit requests against the SAME context
222+
# length as prefill; otherwise decode accepts over-length requests that prefill
223+
# rejects, and those requests hang forever waiting for a KV transfer that never
224+
# comes (the 8k1k conc-500 straggler). Fall back to the prefill value if the
225+
# decode context_length is not set in the model config, so the two always agree.
226+
if [[ -z "$decode_context_length" ]]; then
227+
decode_context_length=$prefill_context_length
214228
fi
215229

216230
# When both DP and EP are enabled, override max-running-requests and dispatch tokens
@@ -251,6 +265,9 @@ DECODE_MODE_FLAGS="--mem-fraction-static ${DECODE_MEM_FRACTION_STATIC} --max-run
251265
if [[ "$DECODE_PREFILL_ROUND_ROBIN_BALANCE" == "True" ]] || [[ "$DECODE_PREFILL_ROUND_ROBIN_BALANCE" == "true" ]]; then
252266
DECODE_MODE_FLAGS="$DECODE_MODE_FLAGS --prefill-round-robin-balance"
253267
fi
268+
if [[ -n "$decode_context_length" ]]; then
269+
DECODE_MODE_FLAGS="$DECODE_MODE_FLAGS --context-length ${decode_context_length}"
270+
fi
254271

255272
if [[ "$DECODE_MTP_SIZE" -gt 0 ]]; then
256273
MORI_MAX_DISPATCH_TOKENS_DECODE=$((MORI_MAX_DISPATCH_TOKENS_DECODE * (DECODE_MTP_SIZE + 1)))
@@ -506,12 +523,23 @@ if [ "$NODE_RANK" -eq 0 ]; then
506523
fi
507524
echo "Congratulations!!! All prefill and decode servers are up . . ."
508525

526+
# Circuit-breaker recovery tuning (run 28696443568):
527+
# With defaults the per-worker circuit stays OPEN for cb-timeout-duration-secs=60
528+
# before a half-open retrial. In that run every request 503'd from request #1
529+
# ("all circuits open or unhealthy") for ~31s and lm_eval (max_retries=5) then gave
530+
# up -- i.e. the circuit was still open when the client budget ran out, so 0 result
531+
# files were produced. Shortening the open->half-open window (and letting the router
532+
# itself retry a failed worker selection) lets a transient trip re-close INSIDE the
533+
# client retry budget instead of nuking the whole eval. The breaker stays fully
534+
# ENABLED (thresholds unchanged); this only speeds recovery.
535+
ROUTER_CB_ARGS="${ROUTER_CB_ARGS:---cb-timeout-duration-secs 15 --retry-max-retries 3}"
509536
ROUTER_CMD="python -m sglang_router.launch_router \
510537
--pd-disaggregation \
511538
--port 30000 \
512539
--policy random \
513540
--prefill-policy random \
514541
--decode-policy random \
542+
${ROUTER_CB_ARGS} \
515543
${PREFILL_ARGS} \
516544
${DECODE_ARGS}"
517545

@@ -557,6 +585,39 @@ if [ "$NODE_RANK" -eq 0 ]; then
557585
wait_or_die "$prefill0_pid" bash -c "$HEALTH_BARRIER_CMD" || exit 1
558586
fi
559587

588+
# ---- End-to-end router readiness canary (run 28696443568) ----
589+
# The /readiness barrier above only proves the router PROCESS is up; it does
590+
# NOT prove the router can reach a prefill worker and complete a generation.
591+
# In that run the eval fired the instant /readiness passed and EVERY request
592+
# 503'd ("No available prefill workers (all circuits open or unhealthy)") from
593+
# request #1 -> lm_eval gave up -> 0 result files -> "Verify eval scores" failed.
594+
# Gate the benchmark on ONE successful generation THROUGH the router so the eval
595+
# never starts against a router whose prefill path is not yet actually serving.
596+
if [[ "${ROUTER_READINESS_CANARY:-1}" == "1" ]]; then
597+
CANARY_URL="http://${NODE0_ADDR}:30000/v1/chat/completions"
598+
CANARY_MODEL="${MODEL_DIR}/${MODEL_NAME}"
599+
canary_ok=0
600+
canary_deadline=$(( $(date +%s) + ${ROUTER_CANARY_TIMEOUT:-600} ))
601+
while [ "$(date +%s)" -lt "$canary_deadline" ]; do
602+
canary_code=$(curl -s -o /tmp/router_canary.out -w '%{http_code}' \
603+
-m "${ROUTER_CANARY_REQ_TIMEOUT:-120}" \
604+
-X POST "$CANARY_URL" -H 'Content-Type: application/json' \
605+
-d "{\"model\":\"${CANARY_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"temperature\":0}" 2>/dev/null)
606+
if [ "$canary_code" = "200" ] && \
607+
! grep -qE "circuits open|server_selection_failed|No available" /tmp/router_canary.out 2>/dev/null; then
608+
canary_ok=1; break
609+
fi
610+
echo "Router readiness canary not ready yet (http=${canary_code}); retrying in 5s . . ."
611+
sleep 5
612+
done
613+
if [ "$canary_ok" -ne 1 ]; then
614+
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."
615+
head -c 800 /tmp/router_canary.out 2>/dev/null
616+
exit 1
617+
fi
618+
echo "Router readiness canary passed (end-to-end generation OK)"
619+
fi
620+
560621
echo "Router is ready for benchmarking"
561622
fi
562623

0 commit comments

Comments
 (0)