[Refactor][Model Runner V2][Multimodal] Move the encoder-only path out of the shared runner - #53176
Open
gty111 wants to merge 9 commits into
Open
[Refactor][Model Runner V2][Multimodal] Move the encoder-only path out of the shared runner#53176gty111 wants to merge 9 commits into
gty111 wants to merge 9 commits into
Conversation
Signed-off-by: Tianyu Guo <guoty@inferact.ai>
Signed-off-by: Tianyu Guo <guoty@inferact.ai>
…input buffers Signed-off-by: Tianyu Guo <guoty@inferact.ai>
gty111
requested review from
ProExpertProg,
WoosukKwon,
houseroad,
mgoin,
njhill,
robertgshaw2-redhat,
tlrmchlsmth,
yewentao256 and
youkaichao
as code owners
August 20, 2026 23:33
njhill
reviewed
Aug 21, 2026
Size the input-buffer barrier by the UVA pool depth instead of max_concurrent_batches: the pool clamps its depth to at least 2, so a slot is recycled every 2 steps even when the engine runs one batch at a time, and this runner never waits on the device. One event per slot generation also lets the host run ahead again, which a single event did not. An encoder tier has no DP peer to agree a padded shape with and no CUDA graph to dispatch, so assert DP=1 and build the batch descriptor locally. Signed-off-by: Tianyu Guo <guoty@inferact.ai>
njhill
reviewed
Aug 21, 2026
njhill
reviewed
Aug 21, 2026
vLLM already uses "encoder_only" for the attention type of encoder-only transformers (AttnTypeStr in config/model.py, is_encoder_only_attention in the CPU attention backend), so the old name read as a statement about the model architecture rather than about an instance that runs only the multi-modal encoder. Also drops an is_encoder_only field from a model-runner stub in the warmup tests: after the earlier commits in this series nothing reads that attribute off a runner, so renaming it would only suggest a reader that no longer exists. Signed-off-by: Tianyu Guo <guoty@inferact.ai>
Size the input-buffer event ring by max_concurrent_batches directly, so buffer_utils no longer needs a getter for the pool depth, and wrap the wait and the record in an `input_tensor_semaphore` context manager. Its docstring, rather than a comment at the call site, now carries the constraint that the guard must be released before the encoder runs. Restore the unconditional V2 branch in the worker: gating it on the runner type let an encoder-only instance fall through to the `elif` that warms up the V1 sampler. The check belongs in warmup_kernels, where it was before. Signed-off-by: Tianyu Guo <guoty@inferact.ai>
gty111
requested review from
ApostaC,
alexm-redhat,
heheda12345,
ivanium,
orozery and
ywang96
as code owners
August 21, 2026 22:01
Signed-off-by: Tianyu Guo <guoty@inferact.ai>
njhill
reviewed
Aug 21, 2026
Signed-off-by: Tianyu Guo <guoty@inferact.ai>
Collaborator
Author
Thanks for the review! All addressed |
njhill
approved these changes
Aug 22, 2026
Member
|
/ci run |
njhill
enabled auto-merge (squash)
August 22, 2026 02:07
|
✅ Triggered Buildkite CI #85144 for commit |
Collaborator
Author
|
/ci run |
|
✅ Triggered Buildkite CI #85146 for commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
An encoder-only instance —
--mm-encoder-only, or the producer side ofencoder-cache disaggregation — runs the vision encoder and publishes the
embeddings. It runs no language model, holds no KV cache and samples no token,
so most of a step does not apply to it. Today that is expressed as
is_encoder_onlybranches scattered through the shared V2 model runner: one inget_kv_cache_spec,_dummy_run,profile_runandcapture_model, two insideexecute_model, and one more inwarmup.py.This moves all of them into a single
MMEncoderModelRunner, selected by theworker. Two consequences:
The shared runner shrinks by deletion.
gpu/model_runner.pyis+3/-33,and the three added lines are only the un-indenting of the surviving
prepare_inputs_embedscall. Nothing in it is restructured: the encodersubclass reaches the same state through the inherited
update_requests/gather_batch_req_state/prepare_inputs.It fixes an illegal memory access in the encoder.
UvaBufferPoolrecyclesits slots every
max_concurrent_batchessteps with no synchronization, andthe device reads the pooled host buffers in place. That is safe for a sampling
step because
AsyncOutput.get_output()'scopy_event.synchronize()— orderedafter all main-stream work by
copy_stream.wait_stream(main_stream)— is aper-step device wait, and step N is popped during N+1 while its slot is not
reused until N+2. An encoder-only step returns
make_empty_encoder_model_runner_output(), built on the host with no D2Hcopy, so it never waits on the device: the host runs ahead, slots recycle
underneath a live
_apply_write_kernel, and it stores through a stalerow_idx.MMEncoderModelRunneris the one runner with that property, so thebarrier lives there and
buffer_utils.pyis untouched.Test plan and results
All on GB200,
Qwen/Qwen3-VL-4B-Instruct,VLLM_USE_V2_MODEL_RUNNER=1.ruff-check,ruff-formatandmypy-3.12pass on every touched file, andpytest tests/v1/ec_connector/unit/ -qgives 149 passed.Normal multimodal serving is unaffected — the path the deletion-only diff has
to preserve:
"The capital of France is",max_tokens=8,temperature=0" Paris. The capital of Spain is Madrid""Circle, red"Encoder-cache disaggregation, end to end.
ECExampleConnectorwithec_role: ec_producerandec_role: ec_consumerover a shared/dev/shmpath.Every request carries a distinct generated image, so each one is a real encode
rather than a cache hit; every tenth is replayed against the consumer to confirm
the published embeddings are usable.
/health200That clears every observed crash point by 1.49×:
Core dumps from those failures show
_apply_write_kernel'stl.storewith agarbage
row_idx, all 32 lanes illegal, in the last block of the grid.AI assistance
AI assistance (Claude) was used for this change: for the investigation that
identified the missing per-step device wait as the IMA's cause, for the
refactor, and for drafting this description. Every changed line was reviewed by
me, and the tests above were run and their output inspected by me.