Repository navigation
[Refactor] Retire the generic deferred MTP path in MRV2 - #9346
Conversation
Signed-off-by: boatman <109857087+sphinxkkkbc@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
This PR appears to belong to: docs/design/module/ar_runtime.md, docs/design/module/model_integration.md. Module owners: @tzhouam @fake0fan @Gaohan123 Routing: @tzhouam via module of the changed files, CODEOWNERS; @fake0fan via module of the changed files; @Gaohan123 via module of the changed files @sphinxkkkbc, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer. Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment. |
amy-why-3459
left a comment
There was a problem hiding this comment.
Thanks for consolidating MTP into MTPState / post_sample and retiring the generic deferred path — the ownership split and the updated CPU coverage look directionally right.
A few concrete points below on the runner gate, the Qwen3-TTS enablement expansion, and the Moss Local always-on state choice. Not blocking on style; mainly want to make sure the retired deferred path does not leave a silent crash or config landmine.
| if multimodal_outputs and run_eager_mtp is not None: | ||
| run_eager_mtp( | ||
| post_sample = getattr(self.model_state, "post_sample", None) | ||
| if post_sample is not None: |
There was a problem hiding this comment.
Previously this was if multimodal_outputs and run_eager_mtp is not None. Dropping the multimodal_outputs check means post_sample always runs when the method exists.
OmniModelState.post_sample is a no-op without _has_mtp, and MTPState.post_sample early-returns when recorded is empty — those cases are fine. But if preprocess recorded MTP rows (_has_mtp + non-empty recorded) and multimodal_outputs is still None/empty here, MTPState.post_sample will hit multimodal_outputs.get(...) (AttributeError) or the "requires retained codes.audio" RuntimeError.
Worth either restoring a soft multimodal_outputs guard at this call site, or hardening MTPState.post_sample to tolerate a missing payload (clear early return / explicit error) so a missing retain path cannot turn into an opaque crash. VoxCPM2's override also now always runs its per-patch end tracking; that looks intentional, but please confirm it is OK when multimodal_outputs is empty.
There was a problem hiding this comment.
Thanks for noting this. Added the empty-MM guard to OmniModelState.post_sample(), so the default implementation only dispatches built-in MTP when MM output is available. The runner still calls the lifecycle hook unconditionally. VoxCPM2 keeps its own override and existing per-step patch decoding and stop handling; it does not go through this guard.
| # its reference context independently of the two-stage codec profile. | ||
| self.stream_ref_context_frames = stream_ref_context_frames(vllm_config) if self.stream_decode else 0 | ||
| self.mtp_eager_frames = talker_first_audio_enabled(vllm_config) or self.stream_decode | ||
| self.has_talker_mtp = True |
There was a problem hiding this comment.
Behavior change vs. the previous mtp_eager_frames = talker_first_audio_enabled(...) or self.stream_decode.
has_talker_mtp = True always flips eager_frames_active in OmniModelState, so current-step MTP + post_sample becomes the only path. make_omni_output still only allocates the token-major codes.audio / codec_frame_valid placeholders under async_chunk and eager_frames_active (_make_eager_omni_output). Without async-chunk (or without that placeholder path), preprocess can record MTP rows while forward returns {} / no retained tensors, and post_sample then fails the retained-output check.
Please confirm every supported Qwen3-TTS MRV2 deploy path either (a) always has async_chunk (or otherwise retains those tensors), or (b) gate has_talker_mtp / eager placeholders so the retired deferred path cannot leave a hard failure for previously-working configs. A short note in the PR / model docs would help too.
There was a problem hiding this comment.
[P1] Preserve request isolation in synchronous Qwen3-TTS
With MRV2, async_chunk=False, and prefix caching disabled, make_omni_output() still gathers the retained prefill-sized codes.audio buffers. Current-step MTP fills the outgoing tensor but does not replace those buffers. For example, after prefill spans of 3 and 4 tokens, the next two-request decode produces a [7, Q] codec tensor for a two-token batch. _slice_pooler_value() sees that the leading dimension does not match the current token count and clones the whole tensor into each request, mixing their codec frames.
Please allocate outputs matching the current token spans whenever current-step MTP is active, including synchronous mode and its metadata, and add a two-request prefill-to-decode regression test.
This is based on source tracing at c427588; no native/GPU reproduction was run.
There was a problem hiding this comment.
Fixed the non-async output assembly issue raised by @amy-why-3459 and detailed by @hsliuustc0106. Qwen3 TTS now allocates codes and validity from the current step’s token spans, rather than stale request-buffer codes, while preserving synchronous reference-code metadata. post_sample() then fills the current frames.
Also fixed the corresponding missing-validity container in Qwen3 Omni’s non-async path. CPU regression tests reproduce the previous failures and now pass for async/non-async and mixed prefill/decode batches, including request isolation.
|
|
||
| state_cls = MossLocalModelState | ||
| return state_cls(vllm_config, self, encoder_cache, device) | ||
| return MossLocalModelState(vllm_config, self, encoder_cache, device) |
There was a problem hiding this comment.
Unconditional MossLocalModelState (and dropping the mrv2_gpu_slot_state gate for first-frame decoder below) is a silent migration for any deploy that omitted / set false mrv2_gpu_slot_state and previously stayed on generic OmniModelState + deferred MTP.
Local state also rejects mtp_inputs in preprocess, so those configs cannot fall back. Please call this out in the PR description / release notes, and confirm the default CUDA yaml (already Local) is the only supported MRV2 Local path going forward — especially for Ascend/NPU where UVA is skipped and Local takes the non-UVA gather path.
There was a problem hiding this comment.
Agreed—this changes state selection for configurations that omitted or explicitly disabled mrv2_gpu_slot_state. I’ll call out that migration in the PR description.
MRV2 Local now always selects MossLocalModelState; its model-owned deferred/eager scheduling remains controlled separately by mrv2_eager_mtp. The default CUDA YAML already selected the dedicated state, so its state selection is unchanged. The repository’s NPU configuration still uses MRV1; this PR does not establish support for a custom NPU MRV2 Local deployment. Removing the first-frame decoder’s slot-state gate follows the unconditional state selection; the other first-audio eligibility checks remain.
| @@ -233,49 +232,6 @@ def test_async_output_preserves_fixed_token_scores_on_host(monkeypatch) -> None: | |||
| torch.testing.assert_close(output.prompt_token_id_logprobs_dict["req-0"], scores) | |||
There was a problem hiding this comment.
test_chunked_fixed_token_scores_are_copied_from_cuda (CUDA L4 hardware test for chunked fixed-token prompt logprobs → host) looks deleted here without a replacement in this PR. It does not appear tied to the MTP refactor. Was this intentional (moved / covered elsewhere), or an accidental drop?
There was a problem hiding this comment.
This test was moved unchanged to tests/worker_v2/test_omni_ar_model_runner_cuda.py due to pre-commit requirement, rather than removed. The separate file avoids combining the original file’s module-level CPU mark with accelerator marks.
Omni ReviewBot triage noteAutomated triage of commit
These are automated triage suggestions only — the final decision belongs to the maintainers. |
…ssion test Signed-off-by: boatman <109857087+sphinxkkkbc@users.noreply.github.com>
amy-why-3459
left a comment
There was a problem hiding this comment.
Thanks for the follow-up on tip 1dd81626 — prior points look addressed from the code side.
- Empty-MM guard in
OmniModelState.post_sample(_has_mtp and multimodal_outputs) looks right, and the new regression coversNone/{}. - Qwen3 TTS / Omni non-async now size codes / validity from the current step's token spans (Omni via
_make_eager_talker_output; TTS viacurrent_codes), which closes the request-isolation hole. The new sync/async regression tests look solid. - Confirmed
test_chunked_fixed_token_scores_are_copied_from_cudalives intests/worker_v2/test_omni_ar_model_runner_cuda.pyon this tip (moved for pre-commit, not dropped). - Moss Local always-on state callout is present in the PR description — thanks.
One tiny doc nit: the description still pins vLLM-Omni Commit to c4275885…; worth bumping to this tip when you next edit the body. No new substantive blockers on this delta from my side.
|
@amy-why-3459 Thanks for checking! I've updated the PR description with the latest SHA, test results, and the MOSS Local state migration details. Full-model inference and performance remain untested. |
Brings in vllm-project#9507, which only appends batched decode rows to the MTP rows when the model state runs current-step MTP. Since vllm-project#9346 every batched row was treated as deferred MTP, so Chatterbox's non-MTP decode hooks failed the first Model Runner V2 decode step; with main's guard they run as they did before vllm-project#9346, and Chatterbox needs no change of its own. Signed-off-by: Will Rice <wrice20@gmail.com>
PLEASE FILL IN THE PR DESCRIPTION HERE.
Contributor intent (required human answers)
1. What outcome do you want to achieve?
Implement #9173 by retiring the generic legacy deferred MTP path in MRV2.
2. What is the highest-leverage variable for that outcome?
The coexistence of deferred and current-step MTP scheduling, together with MTP state distributed across components.
3. How will this work move that variable?
Consolidate MTP execution state in
MTPState, usepost_sample()for the generic current-step path, and remove generic deferred MTP dispatch from preprocess.4. Whose customer or user needs does this address, and what are those needs?
Contributors who need a simpler, more maintainable MTP execution lifecycle.
Purpose
Implements #9173. The motivation is described in the issue; this PR focuses on retiring the generic legacy deferred MTP path, only changes MRV2. Model adaptations are limited to models integrated with MRV2; V1 execution remains unchanged.
What's Changed
Renamed
EagerMTPStatetoMTPStateinmtp_model_state.pyand consolidates MTP-specific buffers, request generators, execution helpers, and CUDA Graph management there. It replacesrun_eager_mtp()withmodel_state.post_sample(), removes generic deferred MTP dispatch from preprocess, and separates decode-input composition metadata from the rows recorded for current-step MTP.Most helpers retain their existing behavior, and model-side
mtp/talker_mtphook names and signatures remain unchanged. Preprocess continues to consume cached frame embeddings and compose the following backbone input.Model Side Change
Qwen3 Omni and Qwen3 TTS use current-step MTP, with first-audio decoding and delivery configured separately.
VoxCPM2 migrates its callback to
post_sample()while retaining its per-patch audio and stop handling.MOSS Local now always uses
MossLocalModelState, andmrv2_gpu_slot_stateis removed. Configurations that previously omitted this option or set it to false now migrate from genericOmniModelStateto the dedicated state. Model-owned deferred/eager scheduling remains controlled bymrv2_eager_mtp. The default CUDA YAML already selected the dedicated state, so its state selection is unchanged. Existing NPU deployments remain on MRV1; custom MRV2 Local deployments on NPU have not been validated.vLLM Version:
v0.32.0(5df63a7203)vLLM-Omni Commit:
1dd816263fb2e5009448620374d943cecd0fdc38I haven't benchmarked performance, but I'd be happy to do so if reviewers would find it useful. This PR primarily refactors MTP scheduling and state ownership rather than introducing performance-oriented changes to the execution path.
Test Plan
Added tests verify that current-step MTP is enabled by default for models declaring the capability, independently of first-audio configuration, and that generic preprocess no longer dispatches deferred MTP. An additional boundary test checks explicit rejection of unsupported multi-token sampling.
Existing tests now access resources through
MTPState, with obsolete fields and duplicate initialization removed. MOSS factory and deployment tests reflect unconditional dedicated-state selection, while existing coverage for frame publication, chunked prefill, EOS handling, request reordering, slot reuse, RNG, graph dispatch, and output ownership is retained.CPU-focused coverage:
Test Result
849 passed, no failed test.
BEFORE SUBMITTING: read CONTRIBUTING.md and run the precheck-pr skill with the code agent for a self-check against project conventions.
Note
To avoid agent spam, vLLM-Omni limits contributors without write access to 5 open PRs. If this blocks well-intentioned critical work, please contact a committer to request bypass list access.
(anything written below this line will be removed by GitHub Actions)
Need help on this PR? Tag
@codesmith-botwith what you need.