Repository navigation
[Perf][TTS] Add per-row generator support to MOSS-TTS talker to eliminate serial fallback for seeded batched requests - #7922
Conversation
|
This PR appears to belong to: docs/design/module/model_integration.md, docs/design/module/ar_runtime.md. Module owners: @gcanlin @Sy0307 @tzhouam Routing: @gcanlin via module of the changed files, CODEOWNERS; @Sy0307 via module of the changed files, CODEOWNERS; @tzhouam via module of the changed files, CODEOWNERS @jingchengtian, 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. |
be6d1c0 to
d138827
Compare
Self-Review (precheck-pr, full mode)Branch: Summary
Verdict: 0 blocking | 2 warnings (environment limitations) | ready for review |
Omni ReviewBot triage noteAutomated triage of commit
These are automated triage suggestions only — the final decision belongs to the maintainers. |
d138827 to
f496647
Compare
NickCao
left a comment
There was a problem hiding this comment.
On CUDA, the model is marked graph-safe:
That causes the runner to wrap talker_mtp in a CUDA graph.
The sequence is:
- During startup, CUDA captures talker_mtp using dummy inputs and no generators.
- A seeded request later passes:
generators=[generator_for_request_1, generator_for_request_2] - The CUDA graph wrapper replays the already-captured GPU graph. It does not call the Python talker_mtp function again, so it never reads those generators.
Thus is not working with cuda graph enabled.
|
@NickCao thanks for the review - you are right. When a batch carries explicit per-row seeds, the runner can no longer replay a graph that was captured without generators. I pushed a follow-up commit (76ea369) that fixes this at the runner level instead of disabling the graph. What changed (
Why this is better than the Qwen3-TTS approach: The pushed function body is byte-identical to the version E2E-verified on Ascend 910B2C:
I'd love your take on whether this per-batch eager gating is acceptable vs. keeping the model-side |
76ea369 to
f496647
Compare
Runner fix moved to #8009Following NickCao's review, the runner-side change that was previously on this branch (force eager talker_mtp when a batch carries explicit seeds) has been split out into a standalone PR: #8009 This PR now contains only the MOSS-TTS model-side per-row generator support (local sampler, local_depth, talker). The runner fix in #8009 is generic: once merged, MOSS-TTS and Qwen3-TTS both honor per-row generators on graph-wrapped platforms. Latest main (PR #7781) already sets |
|
@NickCao thanks for the review. I've reworked the PR exactly as you suggested: What changed: the runner-side graph-replay fix is no longer in this PR. It's been split out into a standalone generic runner PR #8009. This PR now contains only the MOSS-TTS model-side per-row Relationship between the two PRs:
Both are rebased on latest main. #8009's CI is green (pre-commit, build, DCO). End-to-end verification for MOSS-TTS is in the PR description (seeded batches drop from ~3s serial to ~1s parallel, same-seed is byte-identical, no-seed graph replay unchanged). Could you take another look and merge if you're happy? Happy to adjust anything else. |
NickCao
left a comment
There was a problem hiding this comment.
LGTM now that #8009 has landed, one nitpick: the length of generators should be validated, as in
vllm-omni/vllm_omni/model_executor/models/common/qwen3_code_predictor.py
Lines 831 to 840 in 12e9280
|
@NickCao thanks for the nitpick — fixed in 4efd513. What changed
The validation sits before the Tests — new End-to-end on Ascend 910B2C (MOSS-TTS-Local-Transformer-v1.5, real weights, on top of #8009):
One thing I did not claim: that a batched seeded request is byte-identical to serving the same request alone. That does not hold end-to-end on any build, with or without this PR — the Qwen3 backbone's batched prefill logits differ at the last bit from the The branch is still based on the pre-#8009 merge-base and merges cleanly into current Could you take another look and merge if you're happy? Happy to adjust anything else. |
|
|
|
Oops, need a rebase. |
…nate serial fallback for seeded batched requests When B>1 seeded requests are batched, the runner serially processes each row (B×12 serial depth-transformer forward passes) unless the model sets talker_mtp_accepts_per_row_generators. This PR sets that flag and threads generators through _sample_token → generate_frame → talker_mtp, matching the Qwen3-TTS pattern (qwen3_tts_talker.py:351). Each row uses its own torch.Generator for multinomial, preserving per-row reproducibility. The expensive topk/softmax/nucleus filtering stays batched. AI assistance: Used opencode to draft the code, benchmark scripts, and PR description. I reviewed all changes and verified benchmark results from actual NPU runs. Validation: single-module + E2E benchmarks at B/C=1,8,16,32,64. Signed-off-by: jingchengtian <jingchengtian@users.noreply.github.com> Signed-off-by: jingchengtian <tjc1995@126.com>
A ``generators`` list shorter than the batch silently sampled the extra rows from the global RNG, so a mis-sized batch would quietly lose per-row reproducibility. Validate the length up front instead, mirroring ``Qwen3CodePredictor._normalize_generators``: ``_sample_token`` raises ``ValueError`` on a mismatch and ``talker_mtp`` fails on entry (before the n_vq-step depth loop) when the batch does not match. The all-``None`` case still falls through to the single batched ``multinomial``, so unseeded and B=1 requests keep the original path. Tests: new tests/model_executor/models/moss_tts/test_per_row_generators.py (CPU, 6 cases) covers the length check plus per-row reproducibility, batch composition independence and the unseeded fall-through. Validated on Ascend 910B2C with MOSS-TTS-Local-Transformer-v1.5: seeded batches of 4/8/16 return 200 with valid audio, C=4 1.61s vs 2.40s and C=16 2.81s vs 7.53s against the serial fallback, and a batched generate_frame reproduces per-row codes bit-identically to running each row alone. Signed-off-by: jingchengtian <jingchengtian@users.noreply.github.com> Signed-off-by: jingchengtian <tjc1995@126.com>
…tion Two hooks rejected the previous commit: - check-spdx-header: modeling_moss_tts_local.py carried no SPDX header and the new test file copied the stale "vLLM project" copyright line. The hook rewrites both to the vLLM-Omni lines. - typos: "mis-sized" in the talker_mtp comment is not a word it knows. Comment and header only, no functional change. Signed-off-by: jingchengtian <jingchengtian@users.noreply.github.com>
a49e095 to
f34ae58
Compare
1. What This PR Does
1.1 Background: MOSS-TTS Inference Pipeline
The full MOSS-TTS inference pipeline consists of three stages:
The depth transformer is a 1-layer GPT2-style decoder responsible for generating 12 codebook audio codes (n_vq=12) per audio frame. It works autoregressively: first generate codebook 0, then use the result as input to generate codebook 1, and so on through codebook 11. So each audio frame requires 12 depth transformer forward passes.
1.2 The seed Parameter and torch.Generator
When a user sends a TTS request, they can include a
seedparameter for reproducible speech synthesis — the same seed + same text should produce the same audio. vLLM-Omni'sOmniGPUModelRunnercreates a dedicatedtorch.Generatorobject for each seeded request. This object is a deterministic random number stream; whentorch.multinomialuses it for sampling, the result is entirely determined by the generator's state.1.3 The Core Problem: Serial Fallback for B>1 Seeded Requests
When vLLM's scheduler batches B requests in the same step and multiple of them carry seeds, the runner faces a problem:
torch.multinomialonly accepts a single generator parameter — you cannot pass B generators at once. If you use one generator for B rows, each row's result would depend on the presence or absence of other rows, breaking per-row reproducibility.The runner's code (
gpu_model_runner.py:1911-1936) handles this as follows:That is, when
talker_mtp_accepts_per_row_generators = False(the original default for MOSS-TTS), the runner splits B rows into B separate_talker_mtp_forwardcalls. Each call runs 12 depth transformer iterations, so the total is:B requests × 12 iterations = B×12 serial depth transformer forward passes
Each forward pass processes only 1 row (batch_size=1), severely underutilizing the NPU's matrix multiply units.
1.4 Qwen3-TTS Already Solved This
Qwen3-TTS's talker (
qwen3_tts_talker.py:351) already setstalker_mtp_accepts_per_row_generators = Trueand threads thegeneratorslist through itscode_predictor. When the runner sees this flag is True, it skips the serial fallback and passesgenerators=row_generatorsdirectly totalker_mtp— one batched call processing all B rows at once.MOSS-TTS Local never implemented this.
1.5 Unaffected Scenarios
generators=None, so the original batchedmultinomialpath is used — completely unaffectedgeneratorpath (not thegeneratorslist), which does not trigger the serial fallback or this PR's logicvllm bench serve: Does not pass seed parameters, so T1 has no effect on standard benchmarks2. Testing Methodology
2.1 Test Environment
moss_tts_local.yaml2.2 Strict Variable Control
Fixed Randomness
common_voice_en_125386.wav) used throughoutWarmup Before Measurement
torch.npu.synchronize()before and after each callMultiple Rounds Averaged
2.3 Single-Module Benchmark
Results
2.4 E2E Benchmark
Results
Note on B=1 Single-Module Result
The single-module benchmark shows a 6.5% slowdown at B=1 (7.436ms vs 6.982ms). This is not a regression caused by this optimization's code logic — it is an artifact of the benchmark harness. The benchmark script forces the B=1 AFTER case to pass
generators=[g](a list), which routes_sample_tokeninto the per-rowmultinomialbranch (Python loop +torch.catfor 1 element). In real serving, the runner passes a scalargeneratorfor B=1 (gpu_model_runner.py:2000-2002), so_sample_tokentakes the originalelsebranch (directmultinomial, no list overhead). The E2E C=1 result confirms this: 1.22s vs 1.20s (1.7%, within noise) — the per-round values overlap between BEFORE and AFTER, proving no real difference at B=1.