[Bugfix][Qwen3-Omni] Handle short Code2Wav chunk outputs (#3687)

Signed-off-by: Sy03 <1370724210@qq.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
Sy03andamy-why-3459 authored and GitHub committed 2026-05-18 18:59:23 +00:00
1 parent 821286794f
commit ab59673f21
2 files changed
+45 -13

No files matched your search

@@ -74,6 +74,14 @@ class SyntheticDecoder(nn.Module):
return self.out(x).clamp(min=-1, max=1)
class ShortOutputDecoder(SyntheticDecoder):
"""Decoder variant that returns fewer samples than seq_len * total_upsample."""
def forward(self, codes):
out = super().forward(codes)
return out[..., :-5] if out.shape[-1] > 5 else out
@pytest.fixture(scope="module")
def decoder():
"""Create a synthetic decoder on CUDA with fixed weights."""
@@ -228,6 +236,35 @@ def test_chunked_decode_output_survives_later_replay(wrapper):
torch.testing.assert_close(graph_out, expected, atol=0, rtol=0)
def test_chunked_decode_preserves_short_chunk_concat_semantics():
"""Chunked decode must allow shorter-than-nominal chunk outputs.
Qwen3-Omni non-async startup can hit a decoder path where a chunk returns
fewer waveform samples than ``code_len * total_upsample``. The wrapper must
preserve the original eager concat behavior instead of copying into exact
nominal slices.
"""
torch.manual_seed(123)
short_decoder = ShortOutputDecoder().to(DEVICE).eval()
short_wrapper = CUDAGraphDecoderWrapper(
decoder=short_decoder,
capture_sizes=[50],
capture_batch_sizes=[1],
num_quantizers=NUM_QUANTIZERS,
enabled=True,
)
short_wrapper.warmup(DEVICE)
codes = _random_codes(100)
with torch.no_grad():
eager_out = _eager_chunked(short_decoder, codes, chunk_size=50, left_context_size=0)
graph_out = short_wrapper.chunked_decode_with_cudagraph(codes, chunk_size=50, left_context_size=0)
assert graph_out.shape == eager_out.shape
assert graph_out.shape[-1] < 100 * TOTAL_UPSAMPLE
torch.testing.assert_close(graph_out, eager_out, atol=0, rtol=0)
def test_batched_chunked_decode_variable_lengths_matches_per_request_eager(decoder, wrapper):
"""Variable-length chunk batching should match independent chunked decodes."""
long_codes = _random_codes(100)
@@ -554,10 +554,10 @@ class CUDAGraphDecoderWrapper:
chunk_size: int = 300,
left_context_size: int = 25,
) -> torch.Tensor:
wavs = []
start_index = 0
total_len = codes.shape[-1]
total_upsample = self.decoder.total_upsample
wav_out = None
while start_index < total_len:
end_index = min(start_index + chunk_size, total_len)
@@ -566,21 +566,16 @@ class CUDAGraphDecoderWrapper:
codes_chunk = codes[..., start_index - context_size : end_index]
wav_chunk = self._decode(codes_chunk, clone_graph_output=False)
if wav_out is None:
wav_out = torch.empty(
(*wav_chunk.shape[:-1], total_len * total_upsample),
dtype=wav_chunk.dtype,
device=wav_chunk.device,
)
src_start = context_size * total_upsample
dst_start = start_index * total_upsample
dst_end = end_index * total_upsample
wav_out[..., dst_start:dst_end].copy_(wav_chunk[..., src_start:])
# Keep origin/main's concat semantics: Qwen3-Omni can return a chunk
# that is shorter than the nominal code_len * total_upsample length.
# Clone each slice because graph outputs are static buffers that later
# replays may overwrite.
wavs.append(wav_chunk[..., context_size * total_upsample :].clone())
start_index = end_index
if wav_out is None:
if not wavs:
return self.decoder(codes)
return wav_out
return torch.cat(wavs, dim=-1)
def batched_chunked_decode_with_cudagraph(
self,