[Bugfix][Router] Sanitize prefiller payload in disagg-prefill (stream, min_tokens)#971
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the send_request_to_prefiller function in request.py to explicitly disable streaming for prefill requests by setting stream to False and removing stream_options from the request data. This prevents aiohttp ContentTypeError exceptions when parsing the response as JSON, since a prefill request with max_tokens=1 does not require Server-Sent Events (SSE). There are no review comments, and I have no additional feedback to provide.
cfb53ab to
52d2fec
Compare
…, min_tokens)
In disaggregated-prefill mode, send_request_to_prefiller forces
max_tokens=1 but otherwise inherits the client's request body. Two
fields can break the prefill step:
1) stream
When the client sends a streaming request (stream=true), the prefiller
returns text/event-stream, which aiohttp's response.json() refuses to
decode by default, raising:
aiohttp.client_exceptions.ContentTypeError: 200, message='Attempt to
decode JSON with unexpected mimetype: text/event-stream; charset=utf-8'
As a result, every streaming request fails the prefill step. This is
trivially reproducible with 'vllm bench serve' (which sends stream=true
by default).
The orchestrated path (route_orchestrated_disaggregated_request) already
handles this correctly by forcing stream=False on the prefill request.
This change ports the same fix to send_request_to_prefiller so both code
paths behave consistently. Streaming is pointless here anyway since
max_tokens=1, and the prefill response body is not consumed by the
caller in the disagg-prefill flow.
2) min_tokens
vLLM's SamplingParams rejects requests with min_tokens > max_tokens. If
the client sends min_tokens > 1, the prefiller would return 400 since
we just forced max_tokens=1. Drop min_tokens from the prefill payload
to keep the prefill step robust against arbitrary client inputs.
Together these two changes make the prefiller payload safe regardless
of what the client sent, while leaving the original request_json used
by the decode phase untouched (req_data is a shallow copy).
Signed-off-by: liucunzhuang <80206067+lcz1998@users.noreply.github.com>
52d2fec to
d6b7c1f
Compare
|
@ruizhang0101 could you take a look when you have a moment? This PR is blocked on the first-time-contributor "Approve and run workflows" gate plus a maintainer review. It's a small (+5/-0) bugfix that makes |
Sorry, I accidentally closed and reopened the PR. It looks like the workflows require maintainer approval again. @ruizhang0101 could you please approve/run the checks again and merge if everything is green? Thanks! |
Problem
In disaggregated-prefill mode (
route_disaggregated_prefill_request),send_request_to_prefillerforcesmax_tokens=1but otherwise inherits the client's request body. Two fields in that body can break the prefill step.1)
stream→aiohttp.ContentTypeErroron every streaming requestWhen the client sends a streaming request (
stream: true), the prefiller (vLLM) returnstext/event-stream, whichaiohttp'sresponse.json()refuses to decode by default (it requiresapplication/jsonmimetype):Trivially reproducible with
vllm bench serve(which sendsstream=trueby default):vllm bench serve \ --backend openai --base-url http://router:8005 \ --endpoint /v1/completions --model qwen --tokenizer "$MODEL_DIR" \ --dataset-name random --random-input-len 8192 --random-output-len 3000 \ --num-prompts 50 --max-concurrency 4 --request-rate inf \ --temperature 0 --ignore-eosEvery streaming request fails the prefill step.
2)
min_tokens→400from prefiller whenmin_tokens > 1vLLM's
SamplingParamsrejects requests wheremin_tokens > max_tokens. We just forcedmax_tokens=1, so any client that legitimately setsmin_tokens > 1(a vLLM extension) would get a 400 from the prefiller. The decode phase wouldn't even start.Root cause
send_request_to_prefilleronly sanitizesmax_tokens/max_completion_tokens. Other fields that could conflict with the forcedmax_tokens=1semantics —stream,stream_options,min_tokens— are passed through as-is.The orchestrated path (
route_orchestrated_disaggregated_request, request.py:779-782) already stripsstream/stream_optionsfor the same reason.send_request_to_prefillersimply missed the same treatment.Fix
Sanitize the prefiller payload (on the local copy
req_data, so the decode-phaserequest_jsonis untouched):Why this is safe
req_data = req_data.copy()is a shallow copy; assignments / pops on top-level scalar keys do not affect the caller'srequest_json(the decode phase still sees the originalstream,min_tokens, etc.).max_tokens=1already makes streaming pointless; no information is lost.await-ed for completion), so forcing non-streaming changes nothing observable.kv_transfer_paramsfrom the response) was already non-streaming and is unchanged by this PR.Verification
black/isortclean on the edited file (matches.pre-commit-config.yamlversions).vllm bench servereproducer above runs end-to-end against a disagg-prefill router withoutContentTypeErrors.-swhen doinggit commit[Bugfix],[Feat], and[CI].