Repository navigation
[Bugfix][Qwen3-Omni] Fix Code2Wav CUDA-graph output length surplus (#4466) - #4525
Conversation
…llm-project#4466) The inner Code2Wav CUDA graph (`CUDAGraphDecoderWrapper._decode`) trimmed each replay output to the nominal `actual_size * total_upsample`. Qwen3-Omni's Code2Wav forward returns fewer samples than nominal (a fixed causal-conv trim constant), so for chunks whose size is not an exact capture bucket the trim kept that constant's worth of stale buffer-tail samples. In streaming (`async_chunk`) the ramp-up chunks each leak this surplus, adding a fixed ~46 ms of extra samples near the start that the chunk concat turns into the "lag in the middle" reported in vllm-project#4466. Fix: trim relative to the captured buffer length instead of the nominal length, so the graph/compiled replay output matches the eager forward exactly. This is a no-op for decoders whose forward returns the nominal length (e.g. Qwen3-TTS, where the captured length already equals nominal). Verified on an H200-class GPU with Qwen3-Omni-30B-A3B-Instruct (vllm 0.22.0): served CUDA-graph audio becomes sample-identical to eager (length delta 1110 -> 0 samples; cross-correlation lag +23 ms slip -> flat 0). Adds regression tests for both the graph and compiled paths using a short-output decoder (fail before, pass after); full wrapper suite: 65 passed. Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
|
@zsl24 PTAL |
hsliuustc0106
left a comment
There was a problem hiding this comment.
Bugfix looks correct: trim to captured buffer length instead of nominal.
|
Reviewed the Code2Wav CUDA graph length fix. The core fix looks technically correct. The old The new logic: drop = (padded_size - actual_size) * self.decoder.total_upsample
actual_out_len = max(0, static_output.shape[-1] - drop)
return static_output[..., :actual_out_len]is the right invariant: trim relative to the captured output's true length, not the nominal length. This keeps Qwen3-TTS as a no-op case while making short-output decoders match eager. I also checked the added regression coverage. The new
I ran the wrapper suite on A100 with vLLM 0.23: I also ran an end-to-end Qwen3-Omni graph/eager comparison using current main + this PR on A100: This directly addresses the #4466 failure mode where graph previously produced +1110 samples / +46.25 ms versus eager. One caveat worth keeping visible: Net: the fix is narrow, well targeted, and the tests cover the previously broken replay paths. No request-changes from me. Notice fix the CI issue. |
|
Thanks for the targeted fix. One nearby edge case still seems worth covering before this lands: Qwen3-Omni can pack multiple Code2Wav requests into a padded Could you add a small regression test for a multi-request, variable-length Qwen3-Omni Code2Wav batch using a short-output decoder, or note why that path cannot hit CUDA graph replay in practice? The current new tests cover the single-request padded graph/compiled replay case well; this would close the closest remaining sibling case where one shorter row in a padded batch could still leak or over-slice relative to eager length. |
…llm-project#4466) (vllm-project#4525) Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Sy03 <1370724210@qq.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Summary
Addresses the "noticeable lag in the middle" reported in #4466 when CUDA graph is enabled in Qwen3-Omni Code2Wav (stage 2,
enforce_eager: false).Root cause
CUDAGraphDecoderWrapper._decodetrimmed each graph/compiled replay output to the nominalactual_size * total_upsample. Qwen3-Omni's Code2Wavforwardreturns fewer samples than nominal (a fixed causal-conv trim constant; empiricallyS * 1920 - 555). For a chunk whose size is not an exact capture bucket (zero-padded up to the bucket), trimming to nominal keeps that constant's worth of stale buffer-tail samples.In streaming (
async_chunk: true) the ramp-up chunks each leak this surplus, adding a fixed ~46 ms of extra samples near the start that the chunk concat turns into a one-time timing slip, heard as the "lag in the middle". Steady-state chunks are unaffected becausechunk + left_contextis always a capture bucket by construction, so the effect does not accumulate.This was partially addressed by #3687 for the non-streaming
chunked_decode_with_cudagraphpath, but_decodeitself (used by the default streaming path) was never fixed.Fix
Trim relative to the captured buffer length instead of the nominal length, so the graph/compiled output matches the eager forward exactly. This is a no-op for decoders whose forward returns the nominal length (e.g. Qwen3-TTS, where the captured length already equals nominal).
Verification
On an H200-class GPU with Qwen3-Omni-30B-A3B-Instruct (vllm 0.22.0), serving the stock config (CUDA graph) vs an eager stage-2 variant on the same inputs. Stages 0 and 1 are identical and seeded, so the diff isolates the Code2Wav graph:
This matches the reporter's attached files exactly: their
cuda_graphtrack is +1110 samples vseagerwith the same +23 ms slip.Tests: adds regression tests for both the graph and compiled paths using a short-output decoder (fail before this change, pass after). Full wrapper suite: 65 passed.
Note on #4309
#4309 bundles two symptoms. The "plosive/lag between chunks from CUDA graph" part is this same surplus. The separate speaker-drift part tracks the CUDA toolkit version (the reporter found that moving from CUDA 13 to 12.9 fixes it) and did not reproduce on the H200-class box used here, so it is not addressed by this PR.
@amy-why-3459 would you be able to pull this branch and confirm the lag is gone on your setup? The change is contained to the Code2Wav CUDA graph trim, so the rest of the pipeline is unchanged.