Repository navigation
feat(coordinator): add mm_payload render modes (tensors, none) - #3756
Conversation
Add a return_mm_kwargs param to the render step, false by default. The render request then asks vLLM to leave kwargs_data and mm_metadata out of the response, which removes most of the render cost for image requests and the tensors from the encode and prefill bodies. Without the tensors, generate-format encode and prefill requests carry the client's media as content_parts, keyed by the render hash. vLLM rejects content_parts together with features and expands the placeholders itself, so features is omitted and each placeholder span in token_ids is collapsed to one token. A renderer that ignores the field returns the tensors and keeps the features path, which the e2e suite covers since the simulator does not support the field. Fixes llm-d#3750 Signed-off-by: roytman <roytman@il.ibm.com>
mm_payload replaces return_mm_kwargs on the render step and selects what the render response carries for multimodal entries: tensors, metadata or none, default none. metadata and none both ask the renderer to omit the tensors, which halves render and encode times and shrinks the render response by three orders of magnitude. none regresses prefill: without tensors or metadata the prefill worker reprocesses the media from content_parts. metadata removes that regression: prefill rebuilds each entry's mm_metadata from the metadata the encode response publishes in its EC descriptor, so the prefill worker loads the encoder output through the EC connector instead. A request whose entries have no encodable metadata falls back to the none form. Signed-off-by: roytman <roytman@il.ibm.com>
The uint64 conversion wraps by design: PutUint64 writes the int64 two's-complement bytes vLLM reads back, so no value is lost. Signed-off-by: roytman <roytman@il.ibm.com>
The two values sent identical render and encode requests and differed only in prefill's wire form, and metadata already fell back to none's form per request wherever the rebuild could not apply. none now omits the tensors and lets prefill send features with mm_metadata when every entry has metadata and EC coverage, else content_parts. Metadata from the render response is preferred over the EC rebuild. The rebuild is a stopgap until vLLM keeps mm_metadata when the tensors are omitted. Signed-off-by: roytman <roytman@il.ibm.com>
The generate format's content_parts path preprocesses media at the deployment defaults, so a body setting mm_processor_kwargs or media_io_kwargs cannot run on it: render tokenizes with the client's kwargs, while the encoder and the worker preprocess without them. Such requests are rendered with the tensors, whose output render already processed with the client's kwargs. A text-only generate prefill request omits features instead of sending a null map, and the metadata rebuild encodes only the image_grid_thw and video_grid_thw fields whose layout it knows; any other field takes the content_parts fallback. Signed-off-by: roytman <roytman@il.ibm.com>
Thanks for the careful read. The release note now names vLLM v0.29 or later. The prompt_token_parity point is in the description: under |
ahg-g
left a comment
There was a problem hiding this comment.
The render savings look good. Requesting changes on the generate-format path: defaulting to none breaks models whose placeholder spans mix tokens and slows prefill with ec-shared-storage. Details inline.
| } | ||
| return nil | ||
| } | ||
| if withMetadata, ok := entriesWithECMetadata(entries, ecParams); ok { |
There was a problem hiding this comment.
Document that ec-shared-storage always gets the content_parts fallback under none, or allow the metadata form when the connector has no descriptors.
Why: sharedStorageEC.PreparePrefillECParams returns an empty map and MergeEncodeResponse drops the descriptor, so entriesWithECMetadata never succeeds. Every request then re-runs the image processor on prefill: +120 ms for one image, +380 ms for four in the PR's own table. ec-shared-storage is the default ec_connector in config/coordinator/coordinator.yaml, so a default generate-format deployment gets the slow prefill on every request.
There was a problem hiding this comment.
With tensors as the default (see 1), a default deployment no longer reaches this path, ec-shared-storage included. Deployments that opt into none with ec-shared-storage hit a gap: the connector publishes no EC descriptors (PreparePrefillECParams returns an empty map and MergeEncodeResponse is a no-op), so the coordinator has nothing to rebuild mm_metadata from, and prefill always takes the content_parts fallback. That is now documented in docs/communication.md.
The metadata form can't be allowed without descriptors. Prefill needs mm_metadata (the grid) to expand placeholders and compute mRoPE after the EC load, and only the descriptor or the render response can supply it. The fix that works for every connector is vllm-project/vllm#64997, which keeps mm_metadata in the render response when the tensors are omitted.
For reference, the fallback's cost was measured with ec-nixl. I haven't measured ec-shared-storage, which has different load costs.
| // MMPayload is the render step's mm_payload when render returned no | ||
| // kwargs_data for the entries: MMPayloadNone. Empty when the entries | ||
| // carry kwargs_data or render did not populate them. | ||
| MMPayload string |
There was a problem hiding this comment.
Replace MMPayload string with TensorsOmitted bool.
Why: the field only ever holds "" or "none", and every reader checks != "". A string suggests more states than exist and makes readers look up what "" means.
There was a problem hiding this comment.
Kept the string for now: the field mirrors the wire enum, and there is a discussion in vllm-project/vllm#64997 and vllm-project/vllm/#65018 about adding a metadata value or extending none. At which point the string holds a third state a bool cannot.
Easy to switch if you'd rather not wait for that.
| } | ||
|
|
||
| // clientMediaParts groups the media parts of the client's own request body. | ||
| func clientMediaParts(reqCtx *pipeline.RequestContext) map[reqcommon.Modality][]mediaPart { |
There was a problem hiding this comment.
Compute partsByMod once per request (e.g. cache it on RequestContext during encode) instead of calling clientMediaParts again on the prefill fallback.
Why: promptItems decodes messages from json.RawMessage on every call. With inlined data: media that is megabytes of base64 decoded and allocated twice per request on the fallback path.
There was a problem hiding this comment.
I don't think the double decode happens on this path. replace-media-urls runs before encode in every shipped pipeline, decodes items once, and writes the decoded slice back into the body when it finds media refs, which happens on every multimodal request. Encode's and prefill's promptItems calls then hit the []any case: a type assertion and a walk over part maps that alias the same objects, so no media bytes are decoded or copied twice. Encode also skips generate-path requests entirely, the only ones whose body would stay raw.
A pipeline configured without replace-media-urls would decode per call; if that configuration matters, I'm happy to cache partsByMod on the request context as a follow-up.
Signed-off-by: roytman <roytman@il.ibm.com>
Signed-off-by: roytman <roytman@il.ibm.com>
ahg-g
left a comment
There was a problem hiding this comment.
Thanks for the updates; defaulting to tensors and checking span uniformity on encode resolve the main blockers. A few remaining items inline (notably json.RawMessage("null") handling in bodySetsProcessorKwargs and duplicate constants across packages), plus the PR title, description, test plan, and release-note block still describe none as the default.
Signed-off-by: roytman <roytman@il.ibm.com>
Signed-off-by: roytman <roytman@il.ibm.com>
Signed-off-by: roytman <roytman@il.ibm.com>
What type of PR is this?
/kind performance
What this PR does / why we need it:
Define the coordinator
renderstepmm_payloadparameter, which selects what the render response carries for multimodal entries. Two values:tensors(default): the processed multimodal tensors (kwargs_data,mm_metadata), the behavior before this PR.none(opt-in): only token ids,mm_hashesandmm_placeholders. Generate-format encode requests (use_openai_format: false) carry the client's original media ascontent_partson/inference/v1/generate. The generate-format prefill request carriesfeatureswithmm_metadatawhen every entry has metadata (from the render response, or rebuilt from the encode response's EC descriptors) and EC coverage, andcontent_partsotherwise.noneis opt-in because itscontent_partsform requires every placeholder span to be one repeated token. Checked with each model's processor (one 640x448 image, eight video frames), only Qwen-VL images have that shape (<|image_pad|>x N). Gemma3, Pixtral, Idefics3/SmolVLM and Qwen3-VL video interleave other tokens in the span, and on those models the request fails with an error namingmm_payload noneinstead of sending a prompt the worker would expand differently. Once vllm-project/vllm#54548 carries theis_embedmask in the render response,nonecan become the default.For
nonethe render request carriesmm_payload: "none"plusreturn_mm_kwargs: falseas a deprecated alias (vLLM has no metadata-only render payload; #3273 made the same request from the EPP token producer).tensorssends no field. A renderer without the field ignores it and returns the tensors, and the request takes thefeaturespath. A body that setsmm_processor_kwargsormedia_io_kwargsis rendered with the tensors under either value, because thecontent_partspath preprocesses media at the deployment defaults.Each
content_partsitem is{"type": "image_url" | "audio_url" | "video_url", "url": ..., "uuid": <render mm_hash>}. Theuuidkeeps the item keyed by the render hash, so the encode response'sec_transfer_paramsmatches what prefill looks up. An inlineinput_audiopart is sent as adata:URL.featuresis omitted on the same request, because vLLM rejectscontent_partsandfeaturestogether. vLLM expands the placeholders of acontent_partsprompt itself, so each placeholder span intoken_idsis collapsed to one token: encode sends BOS plus one placeholder token, prefill sends the collapsed prompt with one part per entry.The metadata rebuild covers batched int64
keep_on_cpufields, theimage_grid_thw/video_grid_thwshape the Qwen-VL family uses, and is byte-identical to the item vLLM's own render produces. A request with any entry whose metadata has another shape, or no EC descriptor, takes thecontent_partsform for the whole request, and prefill logs the entry and the reason.Operating
none:ec_connector: ec-shared-storagethe encode response carries no EC descriptors, so every prefill request takes thecontent_partsform. The fallback cost in the table below was measured withec-nixl;ec-shared-storagewas not measured.--mm-processor-kwargs, media IO defaults such asmax_pixels), since each worker preprocesses the client's media again.prompt_token_parity: warn(feat(coordinator): check prompt-token parity between prefill and decode #3751) detects a mismatch and is worth leaving on when enablingnone.content_partsform undernone; whichever of the two PRs lands second carries the adaptation.Changed on the
tensorspath:token_idsfails the request; before, it produced a prompt of token0.reqcommon.FieldUUIDandreqcommon.FieldECDescriptorMetadataconstants in place of its local copies (no behavior change).Unchanged paths:
use_openai_format: true, already forward the client body with its media parts./inference/v1/generatecarry their ownfeaturesand skip the render call.Measured on the
coord-disaggregationguide: Qwen3-VL-32B, one TP=2 pod each for encode, prefill and decode, vLLM nightly5d35ce3934, freshly generated 1920x1080 JPEGs so no request hit the encoder or prefix cache. Chat requests through the full pipeline, one at a time, 20 measured per row after 3 warmups,max_tokens: 32, all rows on the same coordinator build at log level 2. Medians in ms, step times from the coordinator's step timings. The twononerows are the samemm_payloadsetting with the prefillfeaturesform and itscontent_partsfallback; they were measured as separatemetadataandnonevalues on a build where the values were still separate:mm_payload/ prefill formtensorsnone,featureswith metadatanone,content_partsfallbacktensorsnone,featureswith metadatanone,content_partsfallbackEvery row returned identical output token counts, with
prompt_tokens2,057 (one image) and 8,183 (four images) matching the rendered prompt.Render response size, median of 5 calls from a pod in the cluster: 16,727,450 bytes with
tensorsvs 15,402 with the tensors omitted for one image, and 66,906,869 vs 58,575 for four.With the tensors omitted, render and encode take about half the time. The
content_partsfallback regresses prefill, because the prefill worker re-runs the image processor on the client's media before it loads the encoder output from the EC connector. Thefeaturesform avoids that re-run, and the per-request fallback picks it whenever every entry has metadata and EC coverage. Thefeaturesform is within 2 ms oftensorson prefill and 7% (one image) to 17% (four images) faster thantensorsend to end.Requires vLLM with
mm_payload/return_mm_kwargson the render endpoints (vllm-project/vllm#61427, vllm-project/vllm#60728) andcontent_partson/inference/v1/generate(vllm-project/vllm#51478, in v0.29 and later). The metadata-only prefill form follows themm_metadata+ec_transfer_paramscontract of vllm-project/vllm#54659. The coordinator-side metadata rebuild is temporary: vLLM nullsmm_metadatatogether withkwargs_datawhen the tensors are omitted, althoughmm_metadatais tiny. Once vLLM keepsmm_metadatain that response (vllm-project/vllm#64997, or amm_payload: "metadata"value, discussed in vllm-project/vllm#46722 and vllm-project/vllm#65018), the rebuild is dropped and the render response's metadata is used directly.Which issue(s) this PR fixes:
Fixes #3750
Part of #3656
Test plan
mm_payloaddefaults totensorsand sends the render request without the field;nonesendsmm_payload: "none"with thereturn_mm_kwargs: falsealias; an invalid value (includingmetadata) is rejected; neither field leaks into the client body forwarded downstreammm_processor_kwargsormedia_io_kwargsis rendered with the tensors undernone, an explicit JSONnullcounts as unset, and a renderer that returns nokwargs_datafor such a body fails the requestmm_metadataitem is byte-identical to vLLM's msgpack output forimage_grid_thw; non-integer, out-of-range (2^63) or non-batched values are rejectedcontent_partscapped to one token and prefill sendsfeatureswith rebuiltmm_metadata, nullkwargs_dataand the expandedtoken_ids;mm_metadatafrom the render response is sent as-is without EC metadatamm_metadata, takes thecontent_partsform for the whole requestmm_payloadkeeps thefeaturespath; a text-only request leaves the path unchangedimage_url, Responsesinput_imageand Messages base64 image parts; a missing part, a part without a URL, an unsupported audio format, or an unknown modality fails the requesttoken_idsfails the requestfeaturesand nocontent_parts. The simulator cannot omit tensors, so thecontent_partsand metadata-rebuild paths are covered by the unit tests and the GPU run; Honor mm_payload / return_mm_kwargs on the render endpoints llm-d-inference-sim#850 tracks simulator supportgo test -race ./pkg/coordinator/...,make lintandmake lint-securityprompt_tokensRelease note (write
NONEif no user-facing change):