Skip to content

vllm-long-prefill-share

Source: fixes/vllm-long-prefill-share/

Let long prompts take at most a set share of wall time while other requests decode, so one very long prefill does not stall every other stream. Opt-in scheduler feature, off until configured.

Affects long-context deployments (hundreds of thousands of tokens) with chunked prefill and concurrent users
Releases v0.26.0, v0.30.0
Patches vllm/v1/core/sched/scheduler.py
Upstream not filed yet (scheduler feature proposal)
Test test_long_prefill_share.py (stdlib; the source tests need a release's sources)

Problem

With chunked prefill, every scheduler step that carries a chunk of a long prompt runs that chunk's attention over all of the prompt's context so far. Deep into a long prompt one chunk can take seconds, and every other running request gets only one decode token per step. --long-prefill-token-threshold makes the chunk smaller, which helps a new request get its first token, but it cannot help decode, because a long chunk is still in every step.

Symptom

While one prompt of several hundred thousand tokens prefills, other users' streams slow to well under one token per second for minutes, and short new requests wait a long time for their first token. The same requests are fast as soon as the long prefill finishes.

Who is affected

Servers with a large --max-model-len that receive very long prompts next to interactive traffic. Short-context deployments never reach the threshold and are not affected.

The fix

vLLM already defers in-progress prefill chunks on some steps while decodes run (defer_prefills, used for DP prefill balancing). The fix adds a second reason to take that branch: after a step that carried a long chunk and took T seconds, the next T * (1 - share) / share seconds are decode-only for long prompts, as long as another request is decoding or a short request is waiting to start. New and short requests are never deferred, and with nobody else to serve nothing is deferred. Step times are measured where steps complete, which is correct under async scheduling.

The feature is off unless VLLM_LONG_PREFILL_SHARE is set, so applying the fix alone changes nothing:

Variable Meaning Default
VLLM_LONG_PREFILL_SHARE fraction of wall time long prefills may take while others decode, between 0 and 1 unset (off)
VLLM_LONG_PREFILL_MIN_TOKENS prompt length that counts as long 65536
VLLM_LONG_PREFILL_SHARE_ASYNC 1: also defer long chunks while one is still in flight 0

With async scheduling the next step is scheduled before the previous one completes, so by default long chunks run in back-to-back pairs and the decode share is (1 - s) / (1 + s) rather than 1 - s. Set VLLM_LONG_PREFILL_SHARE_ASYNC=1 to make the share exact. Pair the feature with --long-prefill-token-threshold, so that a single long step stays short and decode is smooth rather than stalling for seconds.

Measured trade-off

The time given to the other streams is taken from the long prompts; the total is fixed. Both measurements below are single runs.

Qwen3.5-397B-A17B-FP8 on eight cards, releases/v0.30.0, the recipe's settings (16 sequences, prefix caching on, 4,096-token chunks, VLLM_LONG_PREFILL_SHARE=0.5, VLLM_LONG_PREFILL_SHARE_ASYNC=1, VLLM_LONG_PREFILL_MIN_TOKENS=32768), same pod, share on vs off:

Share off Share 0.5
14 streams decoding while one 250K prompt prefills 1.0 tokens/s per stream 9.7 tokens/s per stream
the 250K prompt's own time to first token about 60 s about 120 s
16 synthetic agent sessions, wall time 284 s 269 s
their later-turn TTFT p50 / p90 3.6 / 7.2 s 4.1 / 14.1 s

GLM-5.3 (an MXFP4 MoE model port) on eight cards, on an older vllm-gaudi base with the port's patches (the fix applied unchanged), 35 minutes of two workers sending short chats while two workers send 44K and 91K prompts back to back, so a long prompt is always prefilling (the worst case for both sides). Chained runs on one pod, VLLM_LONG_PREFILL_SHARE_ASYNC=1 in every run with a share:

Setting Chat decode, median Chat TTFT p50 / p95 Long TTFT p50 / p95 Long prompts done
no share 1.2 tokens/s 105 s (p50) 48 / 103 s 71
share 0.25, 2,048-token chunks, min 64K 23.5 tokens/s 1.2 / 4.0 s 383 / 624 s 12
share 0.5, 4,096-token chunks, min 32K 14.9 tokens/s 0.6 / 14.3 s 148 / 252 s 33
share 0.5, 8,192-token chunks, min 32K 12.8 tokens/s 0.8 / 7.3 s 110 / 324 s 33

0.25 protects the chats best, but the long prompts then queue for minutes. 0.5 with 4,096-token chunks (--long-prefill-token-threshold 4096 or --max-num-batched-tokens 4096) keeps chats at about a third of their idle speed while long prompts finish at half the rate; 8,192-token chunks were slower and burstier for the chats. All runs had 0 errors and 0 preemptions.

Verify

python3 fixes/vllm-long-prefill-share/apply.py --check   # 0 = applied, 1 = not applied, 2 = source drifted

End to end, with VLLM_LONG_PREFILL_SHARE=0.5: start a short streaming chat, then send a prompt well above VLLM_LONG_PREFILL_MIN_TOKENS. The chat's tokens per second should drop by about the configured share instead of collapsing, and the long prompt still completes, more slowly.