Merge remote-tracking branch 'origin/main' into dev/vllm-align

This commit is contained in:
tzhouam committed 2026-02-25 08:15:55 +00:00
commit 22e01c416b
39 files changed
+2763 -518

No files matched your search

@@ -15,7 +15,6 @@ from transformers.models.qwen3_omni_moe.configuration_qwen3_omni_moe import (
Qwen3OmniMoeThinkerConfig,
)
from vllm.config import VllmConfig
from vllm.forward_context import get_forward_context
from vllm.logger import init_logger
from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding
from vllm.model_executor.models.interfaces import SupportsMRoPE, SupportsMultiModal, SupportsPP
@@ -355,13 +354,14 @@ class Qwen3OmniMoeForConditionalGeneration(
# ========== Stage 3: Code2Wav ==========
elif self.model_stage == "code2wav":
seq_token_counts: list[int] | None = kwargs.get("seq_token_counts")
# Extract codec codes from input
if input_ids.shape[0] % 16 == 0:
ubatch_slices = get_forward_context().ubatch_slices
if ubatch_slices is not None:
max_seq_len = max(ubatch_slices) // 16
batch_size = len(ubatch_slices)
split_codes = torch.split(input_ids, ubatch_slices, dim=0)
if seq_token_counts is not None:
max_seq_len = max(seq_token_counts) // 16
batch_size = len(seq_token_counts)
split_codes = torch.split(input_ids, seq_token_counts, dim=0)
codes = torch.zeros((batch_size, 16, max_seq_len), device=input_ids.device, dtype=input_ids.dtype)
for idx, code in enumerate(split_codes):
seq_len = code.shape[0] // 16
@@ -386,7 +386,7 @@ class Qwen3OmniMoeForConditionalGeneration(
codes = input_ids_flatten.reshape(1, 16, -1)
# Generate audio from codec codes
audio_tensors = self.generate_audio(codes, voice_type)
audio_tensors = self.generate_audio(codes, voice_type, seq_token_counts)
return audio_tensors
@@ -458,16 +458,22 @@ class Qwen3OmniMoeForConditionalGeneration(
# ==================== Audio Generation ====================
def generate_audio(self, code: torch.Tensor, voice_type: str) -> list[torch.Tensor]:
def generate_audio(
self,
code: torch.Tensor,
voice_type: str,
seq_token_counts: list[int] | None = None,
) -> list[torch.Tensor]:
"""
Generate audio waveform from codec codes.
Args:
code: [8, T] - 8-layer RVQ codec codes
code: [batch, num_quantizers, T] - RVQ codec codes
voice_type: Voice type (not used in Qwen3, kept for compatibility)
seq_token_counts: Token count for each request in batch
Returns:
audio_tensor: [1, waveform_len] - Audio waveform
list of audio waveforms
"""
code2wav_dev = self._module_device(self.code2wav)
@@ -491,6 +497,7 @@ class Qwen3OmniMoeForConditionalGeneration(
talker_codes,
chunk_size=25,
left_context_size=25,
seq_token_counts=seq_token_counts,
)
else:
# Use chunked decode for memory efficiency
@@ -498,6 +505,7 @@ class Qwen3OmniMoeForConditionalGeneration(
talker_codes,
chunk_size=300,
left_context_size=25,
seq_token_counts=seq_token_counts,
)
return audio_tensors
@@ -22,7 +22,6 @@ from transformers.models.qwen3_omni_moe.modeling_qwen3_omni_moe import (
SnakeBeta,
)
from vllm.config import VllmConfig # type: ignore
from vllm.forward_context import get_forward_context
from vllm.logger import init_logger # type: ignore
from vllm.model_executor.models.utils import ( # type: ignore
AutoWeightsLoader,
@@ -163,6 +162,7 @@ class Qwen3OmniMoeCode2Wav(nn.Module):
codes: torch.Tensor,
chunk_size: int = 300,
left_context_size: int = 25,
seq_token_counts: list[int] | None = None,
) -> list[torch.Tensor]:
"""
Decode long sequences in chunks to avoid OOM.
@@ -173,6 +173,7 @@ class Qwen3OmniMoeCode2Wav(nn.Module):
codes: [batch, num_quantizers, seq_len] - num_quantizers-layer RVQ codes
chunk_size: Number of codec frames per chunk
left_context_size: Number of overlapping frames for context
seq_token_counts: Token count for each request in batch
Returns:
list[torch.Tensor]: Complete waveform decoded from the input
@@ -197,12 +198,10 @@ class Qwen3OmniMoeCode2Wav(nn.Module):
start_index = end_index
ubatch_slices = get_forward_context().ubatch_slices
if ubatch_slices is not None:
code_seq_lens = [seq_len // self.config.num_quantizers for seq_len in ubatch_slices]
if seq_token_counts is not None:
code_seq_lens = [seq_len // self.config.num_quantizers for seq_len in seq_token_counts]
else:
# Fallback: assume all batch elements share the same sequence length.
# Create one entry per batch so that each element is processed.
code_seq_lens = [codes.shape[-1]] * codes.shape[0]
batch_wav = torch.cat(wavs, dim=-1)
wavs = []
@@ -216,6 +215,7 @@ class Qwen3OmniMoeCode2Wav(nn.Module):
codes: torch.Tensor,
chunk_size: int = 25,
left_context_size: int = 25,
seq_token_counts: list[int] | None = None,
) -> list[torch.Tensor]:
"""
Decode long sequences in chunks to avoid OOM.
@@ -226,21 +226,19 @@ class Qwen3OmniMoeCode2Wav(nn.Module):
codes: [batch, num_quantizers, seq_len] - num_quantizers-layer RVQ codes
chunk_size: Number of codec frames per chunk
left_context_size: Number of overlapping frames for context
seq_token_counts: Token count for each request in batch
Returns:
list[torch.Tensor]: Complete waveform decoded from the input
codes. For ``batch_size == 1``, this is a list containing a
single tensor with shape ``[1, waveform_len]``.
"""
# Decode chunk
wavs = []
batch_wav = self(codes)
ubatch_slices = get_forward_context().ubatch_slices
if ubatch_slices is not None:
code_seq_lens = [seq_len // self.config.num_quantizers for seq_len in ubatch_slices]
if seq_token_counts is not None:
code_seq_lens = [n // self.config.num_quantizers for n in seq_token_counts]
else:
# Fallback: assume all batch elements share the same sequence length.
# Create one entry per batch so that each element is processed.
code_seq_lens = [codes.shape[-1]] * codes.shape[0]
for idx, code_seq_len in enumerate(code_seq_lens):
# TODO: need to optimize algorithms, current only support
@@ -9,6 +9,7 @@ import torch
import torch.nn as nn
from transformers.utils.hub import cached_file
from vllm.config import VllmConfig
from vllm.forward_context import get_forward_context, is_forward_context_available
from vllm.logger import init_logger
from vllm_omni.model_executor.models.output_templates import OmniOutput
@@ -116,6 +117,28 @@ class Qwen3TTSCode2Wav(nn.Module):
def compute_logits(self, hidden_states: torch.Tensor | OmniOutput, sampling_metadata: Any = None) -> None:
return None
def _split_request_ids(self, ids: torch.Tensor, seq_token_counts: list[int] | None = None) -> list[torch.Tensor]:
"""Split concatenated input_ids into per-request segments.
Uses seq_token_counts (injected by the runner via model_kwargs) when
available, falling back to forward-context ubatch_slices when
micro-batching is active. Returns [ids] for single-request batches.
"""
if seq_token_counts is not None and len(seq_token_counts) > 1:
boundaries = [0]
for count in seq_token_counts:
boundaries.append(boundaries[-1] + count)
n = ids.numel()
return [ids[boundaries[i] : min(boundaries[i + 1], n)] for i in range(len(seq_token_counts))]
if is_forward_context_available():
slices = get_forward_context().ubatch_slices
if slices is not None and len(slices) > 1 and not any(hasattr(s, "token_slice") for s in slices):
boundaries = [0]
for s in slices:
boundaries.append(boundaries[-1] + s)
return [ids[boundaries[i] : boundaries[i + 1]] for i in range(len(boundaries) - 1)]
return [ids]
@torch.no_grad()
def forward(
self,
@@ -124,98 +147,129 @@ class Qwen3TTSCode2Wav(nn.Module):
intermediate_tensors: Any = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: Any,
) -> tuple[torch.Tensor, torch.Tensor]:
) -> OmniOutput:
"""Decode codec codes into audio waveform.
input_ids layout: [codec_context_frames, *flat_codes]
input_ids layout per request: [codec_context_frames, *flat_codes]
where flat_codes is codebook-major [q*F].
When batched, uses forward context ubatch_slices to split the
concatenated input_ids and decode via a single batched forward pass.
"""
tok = self._ensure_speech_tokenizer_loaded()
self._ensure_speech_tokenizer_loaded()
assert self._num_quantizers is not None
assert self._decode_upsample_rate is not None
assert self._output_sample_rate is not None
sr_val = self._output_sample_rate
empty_ret = (
torch.zeros((0,), dtype=torch.float32),
torch.tensor(sr_val, dtype=torch.int32),
)
if input_ids is None:
return empty_ret
tok = self._speech_tokenizer
q = int(self._num_quantizers)
ids = input_ids.reshape(-1).to(dtype=torch.long)
n_tokens = ids.numel()
upsample = int(self._decode_upsample_rate)
sr_val = int(self._output_sample_rate)
sr_tensor = torch.tensor(sr_val, dtype=torch.int32)
empty = torch.zeros((0,), dtype=torch.float32)
if n_tokens == 0:
return empty_ret
# input_ids[0] = codec_context_frames (prepended by stage_input_processor).
ctx_frames = int(ids[0].item())
ids = ids[1:]
n_tokens = ids.numel()
if n_tokens == 0:
return empty_ret
# Warmup / dummy_run: not divisible by num_quantizers.
if n_tokens % q != 0:
logger.warning(
"Code2Wav input_ids length %d not divisible by num_quantizers %d, "
"likely a warmup run; returning empty audio.",
n_tokens,
q,
if input_ids is None or input_ids.numel() == 0:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": [empty], "sr": [sr_tensor]},
)
return empty_ret
total_frames = n_tokens // q
ids = input_ids.reshape(-1).to(dtype=torch.long)
request_ids_list = self._split_request_ids(ids, kwargs.get("seq_token_counts"))
# Reshape codebook-major flat [q*F] -> [q, F] -> [F, q] for SpeechTokenizer.
codes_fq = ids.reshape(q, total_frames).transpose(0, 1).contiguous()
# Parse each request: extract ctx_frames, validate, reshape codes.
# input_ids layout per request: [codec_context_frames, *flat_codes]
# where flat_codes is codebook-major [q*F].
parsed = [] # (ctx_frames, actual_frames)
valid_codes = []
valid_indices = []
for i, req_ids in enumerate(request_ids_list):
if req_ids.numel() < 2:
parsed.append((0, 0))
continue
ctx_frames = int(req_ids[0].item())
flat = req_ids[1:]
n = flat.numel()
# Warmup / dummy_run: not divisible by num_quantizers.
if n == 0 or n % q != 0:
if n > 0:
logger.warning(
"Code2Wav input_ids length %d not divisible by num_quantizers %d, "
"likely a warmup run; returning empty audio.",
n,
q,
)
parsed.append((0, 0))
continue
frames = n // q
# Reshape codebook-major flat [q*F] -> [q, F] -> [F, q] for SpeechTokenizer.
codes_fq = flat.reshape(q, frames).transpose(0, 1).contiguous()
parsed.append((ctx_frames, frames))
valid_codes.append({"audio_codes": codes_fq})
valid_indices.append(i)
if not self._logged_codec_stats and total_frames > 1:
num_req = len(request_ids_list)
if not valid_codes:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"model_outputs": [empty] * num_req,
"sr": [sr_tensor] * num_req,
},
)
if not self._logged_codec_stats:
self._logged_codec_stats = True
try:
uniq = int(torch.unique(codes_fq).numel())
cmin = int(codes_fq.min().item())
cmax = int(codes_fq.max().item())
head = codes_fq[: min(2, total_frames), : min(8, q)].cpu().tolist()
c = valid_codes[0]["audio_codes"]
logger.info(
"Code2Wav codec: frames=%d q=%d uniq=%d range=[%d,%d] head=%s",
total_frames,
"Code2Wav codec: frames=%d q=%d uniq=%d range=[%d,%d] head=%s batch=%d",
c.shape[0],
q,
uniq,
cmin,
cmax,
head,
int(torch.unique(c).numel()),
int(c.min().item()),
int(c.max().item()),
c[: min(2, c.shape[0]), : min(8, q)].cpu().tolist(),
len(valid_codes),
)
except Exception:
pass
wavs, sr = tok.decode({"audio_codes": codes_fq})
if not wavs:
raise ValueError("SpeechTokenizer code2wav produced empty waveform list.")
audio_np = wavs[0].astype(np.float32, copy=False)
# Batched decode: single forward pass through SpeechTokenizer.
wavs, _ = tok.decode(valid_codes)
if len(wavs) != len(valid_codes):
raise RuntimeError(f"Code2Wav returned {len(wavs)} waveforms for {len(valid_codes)} requests")
# Trim left-context waveform samples (streaming sliding window).
if ctx_frames > 0:
upsample = self._decode_upsample_rate
if upsample is None or upsample <= 0:
raise ValueError(f"Invalid decode upsample rate: {upsample}")
cut = ctx_frames * upsample
if cut < audio_np.shape[0]:
audio_np = audio_np[cut:]
else:
logger.warning(
"Context trim %d >= decoded length %d; returning empty audio.",
cut,
audio_np.shape[0],
)
return empty_ret
# Build per-request outputs, trimming padding and left-context.
audios = [empty] * num_req
srs = [sr_tensor] * num_req
audio_tensor = torch.from_numpy(audio_np).to(dtype=torch.float32).reshape(-1)
sr_tensor = torch.tensor(int(sr), dtype=torch.int32)
return audio_tensor, sr_tensor
for j, idx in enumerate(valid_indices):
ctx_frames, actual_frames = parsed[idx]
audio_np = wavs[j].astype(np.float32, copy=False)
# Trim decoder padding (output may be longer due to batch padding).
expected_len = actual_frames * upsample
if audio_np.shape[0] > expected_len:
audio_np = audio_np[:expected_len]
# Trim left-context waveform samples (streaming sliding window).
if ctx_frames > 0:
cut = ctx_frames * upsample
if cut < audio_np.shape[0]:
audio_np = audio_np[cut:]
else:
logger.warning(
"Context trim %d >= decoded length %d; returning empty audio.",
cut,
audio_np.shape[0],
)
continue
if audio_np.shape[0] > 0:
audios[idx] = torch.from_numpy(audio_np).to(dtype=torch.float32).reshape(-1)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": audios, "sr": srs},
)
def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput:
if isinstance(model_outputs, OmniOutput):