Merge branch 'main' into fix/pr4709-review-followup
This commit is contained in:
525 files changed
+38790
-11106
No files matched your search
@@ -587,9 +587,9 @@ class OmniBagelForConditionalGeneration(BagelForConditionalGeneration):
|
||||
input_ids: torch.Tensor | None,
|
||||
positions: torch.Tensor | None,
|
||||
inputs_embeds: torch.Tensor | None,
|
||||
req_ids: list[str],
|
||||
num_computed_tokens: list[int],
|
||||
num_scheduled_tokens: list[int],
|
||||
req_ids: Sequence[str],
|
||||
num_computed_tokens: Sequence[int],
|
||||
num_scheduled_tokens: Sequence[int],
|
||||
input_ids_buffer: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
||||
"""Restore input_ids so _adjust_positions_for_img2img can locate
|
||||
@@ -599,7 +599,7 @@ class OmniBagelForConditionalGeneration(BagelForConditionalGeneration):
|
||||
input_ids = input_ids_buffer
|
||||
return input_ids, positions
|
||||
|
||||
def flush_pending_metadata(self, req_ids: list[str]) -> None:
|
||||
def flush_pending_metadata(self, req_ids: Sequence[str]) -> None:
|
||||
"""Map pending metadata (batch order) to req_ids after forward().
|
||||
|
||||
Guard: if a request already has metadata with ``image_shape``
|
||||
|
||||
@@ -138,8 +138,8 @@ class Encoder(nn.Module):
|
||||
self.norm = nn.LayerNorm(config.hidden_size)
|
||||
self.patch_size = patch_size
|
||||
if patch_size != -1:
|
||||
aggregator_config = _qwen2_config({**encoder_args, "num_hidden_layers": 4})
|
||||
self.aggregator = Qwen2Model(aggregator_config)
|
||||
config.num_hidden_layers = 4
|
||||
self.aggregator = Qwen2Model(config)
|
||||
self.cls_embed = nn.Parameter(torch.rand(1, 1, config.hidden_size))
|
||||
self.cls_embed.data.normal_(0, 0.02)
|
||||
|
||||
@@ -346,8 +346,9 @@ class AudioVAE(PreTrainedModel):
|
||||
h, _ = self.encoder(waveform)
|
||||
h = h.transpose(1, 2)
|
||||
|
||||
mean = torch.chunk(h, 2, dim=1)[0]
|
||||
latent = mean.transpose(1, 2)
|
||||
mean, scale = torch.chunk(h, 2, dim=1)
|
||||
std = F.softplus(scale) + 1e-4
|
||||
latent = (mean + std * torch.randn_like(mean)).transpose(1, 2)
|
||||
return latent, frame_num
|
||||
|
||||
@torch.inference_mode()
|
||||
|
||||
@@ -6,6 +6,19 @@ from torch import nn
|
||||
from x_transformers.x_transformers import apply_rotary_pos_emb
|
||||
|
||||
|
||||
def _apply_rope_cached(t, cos, sin):
|
||||
"""Bit-exact RoPE for ``scale==1`` / interleaved-half.
|
||||
Uses precomputed ``cos``/``sin`` per seq_len to skip per-step trig and cat."""
|
||||
rot_dim = cos.shape[-1]
|
||||
tr = t[..., :rot_dim]
|
||||
x = tr.reshape(*tr.shape[:-1], -1, 2)
|
||||
rot_half = torch.stack((-x[..., 1], x[..., 0]), dim=-1).reshape_as(tr)
|
||||
out = tr * cos + rot_half * sin
|
||||
if rot_dim < t.shape[-1]: # partial rotary (unused by ming heads); keep general
|
||||
out = torch.cat((out, t[..., rot_dim:]), dim=-1)
|
||||
return out.type(t.dtype)
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
def __init__(self, dim, eps=1e-6):
|
||||
super().__init__()
|
||||
@@ -66,12 +79,33 @@ class Attention(nn.Module):
|
||||
self.to_out.append(nn.Dropout(dropout))
|
||||
self.pe_attn_head = pe_attn_head
|
||||
self.attn_mask_enabled = attn_mask_enabled
|
||||
# cos/sin cache keyed by seq_len (freqs are deterministic in seq_len).
|
||||
self._rope_cache: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
|
||||
# TODO: materialize fused weights eagerly outside any torch.compile
|
||||
# scope (e.g. post-weight-load);
|
||||
self._qkv_w: torch.Tensor | None = None
|
||||
self._qkv_b: torch.Tensor | None = None
|
||||
|
||||
def _qkv(self, x):
|
||||
if self._qkv_w is None:
|
||||
self._qkv_w = torch.cat([self.to_q.weight, self.to_k.weight, self.to_v.weight], dim=0)
|
||||
if self.to_q.bias is not None:
|
||||
self._qkv_b = torch.cat([self.to_q.bias, self.to_k.bias, self.to_v.bias], dim=0)
|
||||
qkv = F.linear(x, self._qkv_w, self._qkv_b)
|
||||
return qkv.split(self.inner_dim, dim=-1)
|
||||
|
||||
def _rope_cos_sin(self, freqs):
|
||||
seq_len = freqs.shape[-2]
|
||||
cached = self._rope_cache.get(seq_len)
|
||||
if cached is None:
|
||||
f = freqs.unsqueeze(1) if freqs.ndim == 3 else freqs # b n d -> b 1 n d for 4D q/k
|
||||
cached = (f.cos(), f.sin())
|
||||
self._rope_cache[seq_len] = cached
|
||||
return cached
|
||||
|
||||
def forward(self, x, mask=None, rope=None):
|
||||
batch_size = x.shape[0]
|
||||
query = self.to_q(x)
|
||||
key = self.to_k(x)
|
||||
value = self.to_v(x)
|
||||
query, key, value = self._qkv(x)
|
||||
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // self.heads
|
||||
@@ -86,14 +120,21 @@ class Attention(nn.Module):
|
||||
|
||||
if rope is not None:
|
||||
freqs, xpos_scale = rope
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
if self.pe_attn_head is not None:
|
||||
on = self.pe_attn_head
|
||||
query[:, :on, :, :] = apply_rotary_pos_emb(query[:, :on, :, :], freqs, q_xpos_scale)
|
||||
key[:, :on, :, :] = apply_rotary_pos_emb(key[:, :on, :, :], freqs, k_xpos_scale)
|
||||
scale_is_one = xpos_scale is None or (not torch.is_tensor(xpos_scale) and xpos_scale == 1.0)
|
||||
if scale_is_one and self.pe_attn_head is None:
|
||||
# Fast path: cached, cat-free RoPE (bit-exact for scale==1, full rotary).
|
||||
cos, sin = self._rope_cos_sin(freqs)
|
||||
query = _apply_rope_cached(query, cos, sin)
|
||||
key = _apply_rope_cached(key, cos, sin)
|
||||
else:
|
||||
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
||||
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
if self.pe_attn_head is not None:
|
||||
on = self.pe_attn_head
|
||||
query[:, :on, :, :] = apply_rotary_pos_emb(query[:, :on, :, :], freqs, q_xpos_scale)
|
||||
key[:, :on, :, :] = apply_rotary_pos_emb(key[:, :on, :, :], freqs, k_xpos_scale)
|
||||
else:
|
||||
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
||||
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
||||
|
||||
if self.attn_mask_enabled and mask is not None:
|
||||
valid_sample_indices = mask.any(dim=1)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Shared by Qwen3-Omni and Qwen3-TTS talker models.
|
||||
|
||||
* SDPA attention (F.scaled_dot_product_attention) with native GQA support
|
||||
* HF-compatible numerics (float32 RMSNorm, float32 RoPE, separate linear layers)
|
||||
* HF-compatible CPU/CUDA numerics with NPU-only fused norm/RoPE fast paths
|
||||
* Per-call embedding buffer to avoid cross-request aliasing
|
||||
* Pre-allocated position_ids (read-only, safe to persist)
|
||||
* torch.compile (epilogue_fusion=False) on inner transformer by default
|
||||
@@ -14,7 +14,7 @@ Shared by Qwen3-Omni and Qwen3-TTS talker models.
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
@@ -24,37 +24,56 @@ from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
|
||||
from vllm_omni.diffusion.layers.custom_op import CustomOp
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_GeneratorLike = torch.Generator | Sequence[torch.Generator | None] | None
|
||||
_UNIFORM_EPS = 1e-20
|
||||
|
||||
if current_omni_platform.is_npu():
|
||||
import torch_npu
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# HF-numerics-compatible layers for code predictor
|
||||
# Portable layers for code predictor
|
||||
# ===================================================================
|
||||
#
|
||||
# These use plain PyTorch ops (nn.Linear, manual RMSNorm in float32,
|
||||
# rotate_half RoPE) to produce outputs numerically identical to the
|
||||
# HuggingFace reference. vLLM's fused kernels (RMSNorm, QKVParallel,
|
||||
# get_rope) introduce small precision differences that compound across
|
||||
# the autoregressive steps of the code predictor, causing severe
|
||||
# audio quality degradation.
|
||||
# HuggingFace reference on CPU/CUDA. vLLM's fused kernels (RMSNorm,
|
||||
# QKVParallel, get_rope) introduce small precision differences that compound
|
||||
# across the autoregressive steps of the code predictor, causing severe audio
|
||||
# quality degradation. The Ascend fused norm/RoPE kernels below are dispatched
|
||||
# by the current device platform.
|
||||
#
|
||||
# See: https://github.com/vllm-project/vllm-omni/issues/2274
|
||||
|
||||
|
||||
class _RMSNorm(nn.Module):
|
||||
"""RMSNorm matching HuggingFace's implementation exactly.
|
||||
|
||||
Computes variance in float32 to avoid bfloat16 precision loss.
|
||||
"""
|
||||
class _RMSNorm(CustomOp):
|
||||
"""RMSNorm with HuggingFace-compatible CPU/CUDA math and an NPU fast path."""
|
||||
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(hidden_size))
|
||||
self.variance_epsilon = eps
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
def forward_npu(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
hidden_states, _ = torch_npu.npu_rms_norm(
|
||||
hidden_states,
|
||||
self.weight,
|
||||
self.variance_epsilon,
|
||||
)
|
||||
return hidden_states
|
||||
|
||||
def forward_cuda(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
return self.forward_native(hidden_states)
|
||||
|
||||
def forward_xpu(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
return self.forward_native(hidden_states)
|
||||
|
||||
def forward_native(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
input_dtype = hidden_states.dtype
|
||||
hidden_states = hidden_states.to(torch.float32)
|
||||
variance = hidden_states.pow(2).mean(-1, keepdim=True)
|
||||
@@ -69,11 +88,8 @@ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
||||
return torch.cat((-x2, x1), dim=-1)
|
||||
|
||||
|
||||
class _RotaryEmbedding(nn.Module):
|
||||
"""RoPE matching HuggingFace's implementation exactly.
|
||||
|
||||
Forces float32 computation for cos/sin, matching HF's torch.autocast(enabled=False).
|
||||
"""
|
||||
class _RotaryEmbedding(CustomOp):
|
||||
"""RoPE with HuggingFace-compatible CPU/CUDA math and cached NPU tables."""
|
||||
|
||||
def __init__(self, config) -> None:
|
||||
super().__init__()
|
||||
@@ -85,8 +101,24 @@ class _RotaryEmbedding(nn.Module):
|
||||
rope_theta = getattr(config, "rope_theta", 10000.0)
|
||||
inv_freq = 1.0 / (rope_theta ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
|
||||
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
||||
if current_omni_platform.is_npu():
|
||||
max_seq = int(getattr(config, "num_code_groups", 0) or 0) + 1
|
||||
positions = torch.arange(max_seq, dtype=torch.float32)
|
||||
freqs = torch.outer(positions, inv_freq)
|
||||
emb = torch.cat((freqs, freqs), dim=-1)
|
||||
self.register_buffer("cos_cached", emb.cos(), persistent=False)
|
||||
self.register_buffer("sin_cached", emb.sin(), persistent=False)
|
||||
|
||||
def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
def forward_npu(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return self.cos_cached[position_ids].to(dtype=x.dtype), self.sin_cached[position_ids].to(dtype=x.dtype)
|
||||
|
||||
def forward_cuda(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return self.forward_native(x, position_ids)
|
||||
|
||||
def forward_xpu(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return self.forward_native(x, position_ids)
|
||||
|
||||
def forward_native(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
# position_ids: [batch, seq_len]
|
||||
inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1)
|
||||
position_ids_expanded = position_ids[:, None, :].float()
|
||||
@@ -142,7 +174,6 @@ class CodePredictorAttention(nn.Module):
|
||||
self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)
|
||||
self.q_norm = _RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
self.k_norm = _RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
if current_omni_platform.is_npu():
|
||||
if self.max_seq > 2048:
|
||||
raise ValueError(
|
||||
@@ -165,8 +196,6 @@ class CodePredictorAttention(nn.Module):
|
||||
bsz: int,
|
||||
seq_len: int,
|
||||
) -> torch.Tensor:
|
||||
import torch_npu
|
||||
|
||||
q_f, k_f, v_f = q, k, v
|
||||
if self.is_gqa:
|
||||
k_f = (
|
||||
@@ -227,10 +256,13 @@ class CodePredictorAttention(nn.Module):
|
||||
# cos/sin are [batch, seq_len, head_dim], need unsqueeze at dim=1 for heads
|
||||
cos = cos.unsqueeze(1) # [batch, 1, seq_len, head_dim]
|
||||
sin = sin.unsqueeze(1)
|
||||
q = (q * cos) + (_rotate_half(q) * sin)
|
||||
k = (k * cos) + (_rotate_half(k) * sin)
|
||||
|
||||
if not current_omni_platform.is_npu():
|
||||
if current_omni_platform.is_npu():
|
||||
q = torch_npu.npu_rotary_mul(q, cos, sin)
|
||||
k = torch_npu.npu_rotary_mul(k, cos, sin)
|
||||
attn_out = self._forward_npu_attention(q, k, v, bsz, seq_len)
|
||||
else:
|
||||
q = (q * cos) + (_rotate_half(q) * sin)
|
||||
k = (k * cos) + (_rotate_half(k) * sin)
|
||||
attn_out = F.scaled_dot_product_attention(
|
||||
q,
|
||||
k,
|
||||
@@ -239,8 +271,6 @@ class CodePredictorAttention(nn.Module):
|
||||
is_causal=True,
|
||||
enable_gqa=self.is_gqa,
|
||||
)
|
||||
else:
|
||||
attn_out = self._forward_npu_attention(q, k, v, bsz, seq_len)
|
||||
|
||||
attn_out = attn_out.transpose(1, 2).reshape(bsz, seq_len, -1)
|
||||
return self.o_proj(attn_out)
|
||||
@@ -287,10 +317,17 @@ class CodePredictorDecoderLayer(nn.Module):
|
||||
residual = hidden_states
|
||||
hidden_states = self.input_layernorm(hidden_states)
|
||||
hidden_states = self.self_attn(hidden_states, position_embeddings)
|
||||
hidden_states = residual + hidden_states
|
||||
|
||||
residual = hidden_states
|
||||
hidden_states = self.post_attention_layernorm(hidden_states)
|
||||
if current_omni_platform.is_npu():
|
||||
hidden_states, _, residual = torch_npu.npu_add_rms_norm(
|
||||
hidden_states,
|
||||
residual,
|
||||
self.post_attention_layernorm.weight,
|
||||
self.post_attention_layernorm.variance_epsilon,
|
||||
)
|
||||
else:
|
||||
hidden_states = residual + hidden_states
|
||||
residual = hidden_states
|
||||
hidden_states = self.post_attention_layernorm(hidden_states)
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
hidden_states = residual + hidden_states
|
||||
return hidden_states
|
||||
@@ -605,6 +642,30 @@ class CodePredictorWrapper(nn.Module):
|
||||
values.add(parsed)
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def _normalize_generators(
|
||||
generator: _GeneratorLike, batch_size: int
|
||||
) -> torch.Generator | list[torch.Generator | None] | None:
|
||||
if generator is None or isinstance(generator, torch.Generator):
|
||||
return generator
|
||||
|
||||
row_generators = list(generator)
|
||||
if len(row_generators) != batch_size:
|
||||
raise ValueError(f"Expected {batch_size} per-row generators, but got {len(row_generators)}.")
|
||||
return row_generators
|
||||
|
||||
@classmethod
|
||||
def _sample_codes_gumbel(cls, logits: torch.Tensor, generator: _GeneratorLike = None) -> torch.Tensor:
|
||||
"""Sample ``logits`` via Gumbel-max with optional per-row generators."""
|
||||
row_generators = cls._normalize_generators(generator, int(logits.shape[0]))
|
||||
u = torch.empty_like(logits, dtype=torch.float32)
|
||||
if isinstance(row_generators, list):
|
||||
for row, row_generator in enumerate(row_generators):
|
||||
u[row : row + 1].uniform_(_UNIFORM_EPS, 1.0 - _UNIFORM_EPS, generator=row_generator)
|
||||
else:
|
||||
u.uniform_(_UNIFORM_EPS, 1.0 - _UNIFORM_EPS, generator=row_generators)
|
||||
return (logits.float() - torch.log(-torch.log(u))).argmax(dim=-1, keepdim=True)
|
||||
|
||||
def _prefix_seq_lens(self, max_seq: int) -> list[int]:
|
||||
all_seq_lens = list(range(2, max_seq))
|
||||
if not self._prefix_graph_seq_lens:
|
||||
@@ -742,6 +803,27 @@ class CodePredictorWrapper(nn.Module):
|
||||
# Forward -- re-prefill + inline sampling
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _multinomial(
|
||||
probs: torch.Tensor,
|
||||
generator: torch.Generator | None,
|
||||
generators: Sequence[torch.Generator | None] | None,
|
||||
) -> torch.Tensor:
|
||||
"""Sample one code per row, optionally with per-row generators.
|
||||
|
||||
Per-row generators keep explicitly-seeded requests deterministic in a
|
||||
multi-row batch: each row consumes draws only from its own generator,
|
||||
so the transformer forward can stay batched (#4883).
|
||||
"""
|
||||
if generators is None:
|
||||
return torch.multinomial(probs, num_samples=1, generator=generator)
|
||||
return torch.cat(
|
||||
[
|
||||
torch.multinomial(probs[row : row + 1], num_samples=1, generator=row_generator)
|
||||
for row, row_generator in enumerate(generators)
|
||||
]
|
||||
)
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(
|
||||
self,
|
||||
@@ -753,9 +835,13 @@ class CodePredictorWrapper(nn.Module):
|
||||
top_k: int = 50,
|
||||
top_p: float = 1.0,
|
||||
generator: torch.Generator | None = None,
|
||||
generators: Sequence[torch.Generator | None] | None = None,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Predict residual codebooks 1..G-1 autoregressively via re-prefill."""
|
||||
bsz = int(layer0_code.shape[0])
|
||||
if generators is not None and len(generators) != bsz:
|
||||
raise ValueError(f"generators must have one entry per row: got {len(generators)} for batch {bsz}")
|
||||
sample_generator: _GeneratorLike = generators if generators is not None else generator
|
||||
num_groups = self._num_groups
|
||||
device = layer0_code.device
|
||||
|
||||
@@ -823,7 +909,6 @@ class CodePredictorWrapper(nn.Module):
|
||||
# Use captured device graph if available, otherwise call compiled fn.
|
||||
device_graph_entry = self._device_graphs.get(graph_key)
|
||||
|
||||
# Run transformer (device graph replay or compiled forward)
|
||||
if device_graph_entry is not None:
|
||||
device_graph_entry[0].replay()
|
||||
hidden_out = device_graph_entry[1]
|
||||
@@ -832,9 +917,20 @@ class CodePredictorWrapper(nn.Module):
|
||||
|
||||
logits = lm_heads[step - 1](hidden_out[:bsz, step, :])
|
||||
|
||||
# Sample next code
|
||||
# Sample next code via Gumbel-max.
|
||||
#
|
||||
# ``argmax_i(logits_i + Gumbel_i)`` with
|
||||
# ``Gumbel_i = -log(-log(u_i)), u_i ~ Uniform(0, 1)`` is
|
||||
# distributionally identical to sampling from ``softmax(logits)``.
|
||||
# In this file the motivations are practical rather than graph
|
||||
# related: it is measurably cheaper than ``softmax + multinomial``
|
||||
# on the B x 2048 shapes used here, it stays well-defined for
|
||||
# degenerate masked rows with a surviving finite entry (and is more
|
||||
# defensive than ``multinomial`` around fully-masked/NaN inputs),
|
||||
# and the helper below can honor either one batch generator or one
|
||||
# generator per seeded row.
|
||||
if stored_mode:
|
||||
# "stored" mode: top-k -> top-p -> softmax -> multinomial
|
||||
# "stored" mode: top-k -> top-p -> Gumbel-max
|
||||
if s_top_k > 0:
|
||||
topk_vals, _ = logits.topk(s_top_k, dim=-1)
|
||||
logits = logits.masked_fill(logits < topk_vals[:, -1:], float("-inf"))
|
||||
@@ -845,17 +941,15 @@ class CodePredictorWrapper(nn.Module):
|
||||
remove_mask = (cumulative_probs - sorted_probs) >= s_top_p
|
||||
sorted_logits[remove_mask] = float("-inf")
|
||||
logits = sorted_logits.scatter(1, sorted_idx, sorted_logits)
|
||||
probs = F.softmax(logits, dim=-1, dtype=torch.float32)
|
||||
code = torch.multinomial(probs, num_samples=1, generator=generator)
|
||||
code = self._sample_codes_gumbel(logits, generator=sample_generator)
|
||||
else:
|
||||
# "per_call" mode: temperature-scaled + top-k
|
||||
# "per_call" mode: temperature-scaled + top-k -> Gumbel-max
|
||||
if use_sampling:
|
||||
scaled = logits * inv_temperature
|
||||
if top_k > 0:
|
||||
topk_vals, _ = scaled.topk(top_k, dim=-1)
|
||||
scaled = scaled.masked_fill(scaled < topk_vals[:, -1:], float("-inf"))
|
||||
probs = F.softmax(scaled, dim=-1, dtype=torch.float32)
|
||||
code = torch.multinomial(probs, num_samples=1, generator=generator)
|
||||
code = self._sample_codes_gumbel(scaled, generator=sample_generator)
|
||||
else:
|
||||
code = logits.argmax(dim=-1, keepdim=True)
|
||||
|
||||
@@ -878,6 +972,18 @@ class CodePredictorWrapper(nn.Module):
|
||||
# Weight loading
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _prepare_npu_weights(self) -> None:
|
||||
from vllm_ascend.utils import maybe_trans_nz
|
||||
|
||||
linear_count = 0
|
||||
with torch.no_grad():
|
||||
# Pack linear weights once for NPU matmul.
|
||||
for module in self.modules():
|
||||
if isinstance(module, nn.Linear):
|
||||
module.weight.data = maybe_trans_nz(module.weight.data)
|
||||
linear_count += 1
|
||||
logger.info("Prepared NPU code predictor weights: linear=%d", linear_count)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights directly (no fused projection remapping needed)."""
|
||||
loaded: set[str] = set()
|
||||
@@ -904,4 +1010,7 @@ class CodePredictorWrapper(nn.Module):
|
||||
weight_loader(param, w)
|
||||
loaded.add(name)
|
||||
|
||||
if current_omni_platform.is_npu():
|
||||
self._prepare_npu_weights()
|
||||
|
||||
return loaded
|
||||
@@ -5,9 +5,8 @@
|
||||
Stage 0: Talker — text prompt → speech tokens (LLM autoregressive).
|
||||
Stage 1: Code2Wav — flow-matching decoder → acoustic features → waveform.
|
||||
* ``sync_process_input_func`` (``text2flow_token_only``) runs when
|
||||
``deploy.async_chunk=false``: stage 1 builds full-sequence flow input
|
||||
from the emitted speech tokens. ``text2flow`` is the
|
||||
``custom_process_input_func`` variant.
|
||||
``deploy.async_chunk=false``: stage 1 allocates placeholder slots while
|
||||
bulk tensors arrive via ``text2flow_full_payload`` on the connector.
|
||||
* ``async_chunk_process_next_stage_input_func`` runs when
|
||||
``deploy.async_chunk=true``: stage 0 streams codec chunks to stage 1
|
||||
through the shared-memory connector.
|
||||
@@ -47,7 +46,6 @@ COSYVOICE3_PIPELINE = PipelineConfig(
|
||||
final_output=True,
|
||||
final_output_type="audio",
|
||||
engine_output_type="latent",
|
||||
custom_process_input_func=f"{_PROC}.text2flow",
|
||||
sync_process_input_func=f"{_PROC}.text2flow_token_only",
|
||||
),
|
||||
),
|
||||
|
||||
Regular → Executable
+20
-4
@@ -22,6 +22,7 @@ generation.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
@@ -170,14 +171,25 @@ _AUDIO_TOKENIZER_PATH_ENVS = (
|
||||
)
|
||||
|
||||
|
||||
def _is_higgs_audio_tokenizer_config(config_path: str) -> bool:
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
return config.get("model_type") == "higgs_audio_v2_tokenizer"
|
||||
|
||||
|
||||
def _normalize_audio_tokenizer_dir(path: str) -> str | None:
|
||||
if not path:
|
||||
return None
|
||||
expanded = os.path.abspath(os.path.expanduser(path))
|
||||
if os.path.isfile(os.path.join(expanded, "config.json")):
|
||||
config_path = os.path.join(expanded, "config.json")
|
||||
if os.path.isfile(config_path) and _is_higgs_audio_tokenizer_config(config_path):
|
||||
return expanded
|
||||
nested = os.path.join(expanded, _K2_OMNIVOICE_SUBDIR)
|
||||
if os.path.isfile(os.path.join(nested, "config.json")):
|
||||
nested_config_path = os.path.join(nested, "config.json")
|
||||
if os.path.isfile(nested_config_path) and _is_higgs_audio_tokenizer_config(nested_config_path):
|
||||
return nested
|
||||
return None
|
||||
|
||||
@@ -201,7 +213,9 @@ def _resolve_audio_tokenizer_dir() -> str | None:
|
||||
filename=f"{_K2_OMNIVOICE_SUBDIR}/config.json",
|
||||
)
|
||||
if isinstance(cached_config, str) and os.path.isfile(cached_config):
|
||||
return os.path.dirname(cached_config)
|
||||
candidate = _normalize_audio_tokenizer_dir(os.path.dirname(cached_config))
|
||||
if candidate is not None:
|
||||
return candidate
|
||||
|
||||
from huggingface_hub.constants import HF_HUB_CACHE
|
||||
|
||||
@@ -240,7 +254,9 @@ def _load_audio_tokenizer():
|
||||
_K2_OMNIVOICE_REPO,
|
||||
allow_patterns=[f"{_K2_OMNIVOICE_SUBDIR}/*"],
|
||||
)
|
||||
audio_tokenizer_dir = os.path.join(repo_path, _K2_OMNIVOICE_SUBDIR)
|
||||
audio_tokenizer_dir = _normalize_audio_tokenizer_dir(repo_path)
|
||||
if audio_tokenizer_dir is None:
|
||||
raise RuntimeError(f"Downloaded {_K2_OMNIVOICE_REPO} does not contain a valid Higgs audio tokenizer")
|
||||
device = current_omni_platform.get_torch_device()
|
||||
model = HiggsAudioV2TokenizerModel.from_pretrained(audio_tokenizer_dir).to(device)
|
||||
return model.eval()
|
||||
|
||||
@@ -222,14 +222,8 @@ class HunyuanModel(HunYuanModel):
|
||||
if self.config.tie_word_embeddings and "lm_head.weight" in name:
|
||||
continue
|
||||
|
||||
if self.quant_config is not None and (scale_name := self.quant_config.get_cache_scale(name)):
|
||||
# Loading kv cache scales for compressed-tensors quantization
|
||||
param = params_dict[scale_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
loaded_weight = loaded_weight[0]
|
||||
weight_loader(param, loaded_weight)
|
||||
continue
|
||||
|
||||
# KV-cache scales are renamed to .attn.{k,v,q}_scale by the
|
||||
# quant_config mapper in the outer AutoWeightsLoader.
|
||||
is_found = False
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
|
||||
@@ -483,19 +483,15 @@ class MammothModa2Qwen2ForCausalLM(nn.Module, SupportsPP):
|
||||
]
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
loaded_params: set[str] = set()
|
||||
if self.quant_config is not None:
|
||||
cache_scale_mapper = self.quant_config.get_cache_scale_mapper()
|
||||
if cache_scale_mapper is not None:
|
||||
weights = cache_scale_mapper.apply(weights)
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
if "rotary_emb.inv_freq" in name:
|
||||
continue
|
||||
|
||||
if self.quant_config is not None and (scale_name := self.quant_config.get_cache_scale(name)):
|
||||
param = params_dict[scale_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
loaded_weight = loaded_weight if loaded_weight.dim() == 0 else loaded_weight[0]
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(scale_name)
|
||||
continue
|
||||
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""MammothModa2 pipeline topology (frozen).
|
||||
|
||||
Stage 0: AR — multimodal understanding + latent generation
|
||||
Stage 1: DiT — latent → image
|
||||
|
||||
For text/image understanding tasks (text output only), use MAMMOTH_MODA2_AR_PIPELINE.
|
||||
"""
|
||||
|
||||
from vllm_omni.config.stage_config import (
|
||||
PipelineConfig,
|
||||
StageExecutionType,
|
||||
StagePipelineConfig,
|
||||
)
|
||||
|
||||
_PROC = "vllm_omni.model_executor.stage_input_processors.mammoth_moda2"
|
||||
|
||||
MAMMOTH_MODA2_PIPELINE = PipelineConfig(
|
||||
model_type="mammoth_moda2",
|
||||
model_arch="MammothModa2ForConditionalGeneration",
|
||||
hf_architectures=("Mammothmoda2Model", "MammothModa2ForConditionalGeneration"),
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
model_stage="ar",
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
final_output=False,
|
||||
owns_tokenizer=True,
|
||||
requires_multimodal_data=True,
|
||||
engine_output_type="latent",
|
||||
),
|
||||
StagePipelineConfig(
|
||||
stage_id=1,
|
||||
model_stage="dit",
|
||||
execution_type=StageExecutionType.LLM_GENERATION,
|
||||
input_sources=(0,),
|
||||
final_output=True,
|
||||
final_output_type="image",
|
||||
owns_tokenizer=False,
|
||||
requires_multimodal_data=False,
|
||||
engine_output_type="image",
|
||||
custom_process_input_func=f"{_PROC}.ar2dit",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Single-stage AR variant for understanding / summarization tasks
|
||||
MAMMOTH_MODA2_AR_PIPELINE = PipelineConfig(
|
||||
model_type="mammoth_moda2_ar",
|
||||
model_arch="MammothModa2ForConditionalGeneration",
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
model_stage="ar",
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
final_output=True,
|
||||
final_output_type="text",
|
||||
owns_tokenizer=True,
|
||||
requires_multimodal_data=True,
|
||||
engine_output_type="text",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1153,17 +1153,14 @@ class MiMoAudioLLMForConditionalGeneration(nn.Module, SupportsMultiModal, Suppor
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
|
||||
loaded_params: set[str] = set()
|
||||
if self.quant_config is not None:
|
||||
cache_scale_mapper = self.quant_config.get_cache_scale_mapper()
|
||||
if cache_scale_mapper is not None:
|
||||
weights = cache_scale_mapper.apply(weights)
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
if name.startswith("model."):
|
||||
name = "model." + name
|
||||
if self.quant_config is not None and (scale_name := self.quant_config.get_cache_scale(name)):
|
||||
# Loading kv cache quantization scales
|
||||
param = params_dict[scale_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
loaded_weight = loaded_weight if loaded_weight.dim() == 0 else loaded_weight[0]
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(scale_name)
|
||||
continue
|
||||
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if name.startswith("input_local_transformer.") or name.startswith("local_transformer."):
|
||||
|
||||
@@ -56,7 +56,6 @@ MIMO_AUDIO_PIPELINE = PipelineConfig(
|
||||
final_output=True,
|
||||
final_output_type="audio",
|
||||
engine_output_type="audio",
|
||||
custom_process_input_func=f"{_PROC}.llm2code2wav",
|
||||
sync_process_input_func=f"{_PROC}.llm2code2wav_token_only",
|
||||
sampling_constraints={"detokenize": False},
|
||||
),
|
||||
|
||||
@@ -22,6 +22,7 @@ from .constants import (
|
||||
DEFAULT_SIGMA,
|
||||
DEFAULT_TEMPERATURE,
|
||||
HISTORY_PATCH_SIZE,
|
||||
INITIAL_LATENT_CHUNK_SIZE,
|
||||
KEY_CFG,
|
||||
KEY_CHUNK_ID,
|
||||
KEY_DECODE_STEP,
|
||||
@@ -239,6 +240,7 @@ class MingTTSConfig:
|
||||
max_decode_steps: int = MAX_DECODE_STEPS
|
||||
|
||||
latent_chunk_size: int = LATENT_CHUNK_SIZE
|
||||
initial_latent_chunk_size: int = INITIAL_LATENT_CHUNK_SIZE
|
||||
latent_left_context: int = LATENT_LEFT_CONTEXT
|
||||
|
||||
text_eos_token_id: int = TEXT_EOS_TOKEN_ID
|
||||
|
||||
@@ -58,6 +58,7 @@ DEFAULT_TEMPERATURE = 0.0
|
||||
|
||||
# Connector / Stage-2 streaming defaults (runtime tuning)
|
||||
LATENT_CHUNK_SIZE = 25
|
||||
INITIAL_LATENT_CHUNK_SIZE = 4
|
||||
LATENT_LEFT_CONTEXT = 0
|
||||
MAX_DECODE_STEPS = 200
|
||||
|
||||
|
||||
@@ -326,6 +326,28 @@ class MingLLMModel(nn.Module):
|
||||
try:
|
||||
from .fm.cfm_cudagraph import CFMGraphExecutor, CFMSampler
|
||||
|
||||
# TODO(perf):
|
||||
# (1) compile per-block (dit_model.blocks) instead of the
|
||||
# whole forward for tighter fusion / fewer graph breaks;
|
||||
# (2) move the compile + a synthetic warmup forward to a post-weight-load hook
|
||||
dit_model = self.flowloss.cfm.model
|
||||
if not getattr(dit_model, "_ming_compiled", False):
|
||||
try:
|
||||
# Scoped options (avoid mutating global inductor config).
|
||||
dit_model.forward = torch.compile(
|
||||
dit_model.forward,
|
||||
fullgraph=False,
|
||||
dynamic=False,
|
||||
options={
|
||||
"triton.cudagraphs": False,
|
||||
"triton.cudagraph_trees": False,
|
||||
},
|
||||
)
|
||||
dit_model._ming_compiled = True
|
||||
logger.info("Ming CFM DiT torch.compile enabled (inductor cudagraphs off).")
|
||||
except Exception as exc:
|
||||
logger.warning("Ming CFM torch.compile failed (%s); using uncompiled DiT.", exc)
|
||||
|
||||
sampler = CFMSampler(self.flowloss.cfm.model, steps=_CFM_STEPS)
|
||||
self._cfm_graph = CFMGraphExecutor(
|
||||
sampler,
|
||||
|
||||
@@ -127,7 +127,7 @@ def _validate_ming_decode_window(
|
||||
continue
|
||||
max_steps = _resolve_runtime_int(info, KEY_MAX_DECODE_STEPS, default_max_decode_steps)
|
||||
min_steps = _resolve_optional_runtime_int(info, KEY_MIN_DECODE_STEPS, 0)
|
||||
min_required = max(min_stop_step + 1, min_steps)
|
||||
min_required = max(min_stop_step + 2, min_steps)
|
||||
if max_steps < min_required:
|
||||
req_id = info.get(KEY_REQUEST_ID, f"idx={i}")
|
||||
raise ValueError(
|
||||
@@ -148,7 +148,7 @@ def _resolve_ming_stop_decision(
|
||||
audio_dummy_token_id: int,
|
||||
text_eos_token_id: int,
|
||||
) -> tuple[str, bool, bool, int, int]:
|
||||
min_required_decode_steps = max(min_stop_step + 1, min_decode_steps)
|
||||
min_required_decode_steps = max(min_stop_step + 2, min_decode_steps)
|
||||
if max_decode_steps < min_required_decode_steps:
|
||||
raise RuntimeError(
|
||||
"Invalid Ming decode window: "
|
||||
|
||||
@@ -172,6 +172,9 @@ def validate_ming_tts_config(cfg: Any) -> None:
|
||||
|
||||
if cfg.latent_chunk_size <= 0:
|
||||
raise ValueError(f"latent_chunk_size must be > 0, got {cfg.latent_chunk_size}.")
|
||||
initial_latent_chunk_size = getattr(cfg, "initial_latent_chunk_size", 0)
|
||||
if initial_latent_chunk_size < 0:
|
||||
raise ValueError(f"initial_latent_chunk_size must be >= 0, got {initial_latent_chunk_size}.")
|
||||
if cfg.latent_left_context < 0:
|
||||
raise ValueError(f"latent_left_context must be >= 0, got {cfg.latent_left_context}.")
|
||||
if cfg.max_decode_steps <= 0:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,359 @@
|
||||
# Copyright 2026 OpenMOSS and the HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MossAudioTokenizer model configuration"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from transformers.configuration_utils import PreTrainedConfig
|
||||
from transformers.utils import logging
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
class MossAudioTokenizerConfig(PreTrainedConfig):
|
||||
r"""
|
||||
This is the configuration class to store the configuration of a [`MossAudioTokenizerModel`]. It is used to
|
||||
instantiate a MossAudioTokenizer model according to the specified arguments, defining the model architecture.
|
||||
|
||||
Instantiating a configuration with the defaults will yield a similar configuration to that of the
|
||||
[VoiceAgentGroup/moss_audio_tokenizer](https://huggingface.co/VoiceAgentGroup/moss_audio_tokenizer) architecture.
|
||||
|
||||
Configuration objects inherit from [`PreTrainedConfig`] and can be used to control the model outputs. Read the
|
||||
documentation from [`PreTrainedConfig`] for more information.
|
||||
|
||||
Args:
|
||||
sampling_rate (`int`, *optional*, defaults to 24000):
|
||||
The sampling rate at which the audio waveform should be digitalized expressed in hertz (Hz).
|
||||
downsample_rate (`int`, *optional*, defaults to 1920):
|
||||
Total downsampling rate from waveform to tokens.
|
||||
causal_transformer_context_duration (`float`, *optional*, defaults to 10.0):
|
||||
Context duration in seconds for causal transformer.
|
||||
encoder_kwargs (`list[dict]`, *optional*):
|
||||
List of encoder module configurations. Each dict specifies a module type and its parameters.
|
||||
decoder_kwargs (`list[dict]`, *optional*):
|
||||
List of decoder module configurations in execution order.
|
||||
quantizer_type (`str`, *optional*, defaults to `"rvq"`):
|
||||
Quantizer type. Options include `"rvq"`, `"spec_rvq"`, `"rlfq"`, `"random_prefix_rlfq"`.
|
||||
quantizer_kwargs (`dict`, *optional*):
|
||||
Configuration for the quantizer including `input_dim`, `rvq_dim`, `output_dim`, `num_quantizers`,
|
||||
`codebook_size`, and `codebook_dim`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
>>> from transformers import MossAudioTokenizerModel, MossAudioTokenizerConfig
|
||||
|
||||
>>> # Initializing a MossAudioTokenizer style configuration
|
||||
>>> configuration = MossAudioTokenizerConfig()
|
||||
|
||||
>>> # Initializing a model (with random weights) from the configuration
|
||||
>>> model = MossAudioTokenizerModel(configuration)
|
||||
|
||||
>>> # Accessing the model configuration
|
||||
>>> configuration = model.config
|
||||
```
|
||||
"""
|
||||
|
||||
model_type = "moss-audio-tokenizer"
|
||||
|
||||
# Backward-compatible alias used by some checkpoints.
|
||||
attribute_map = {"sample_rate": "sampling_rate"}
|
||||
|
||||
sampling_rate: int
|
||||
downsample_rate: int
|
||||
causal_transformer_context_duration: float
|
||||
encoder_kwargs: list[dict[str, Any]]
|
||||
decoder_kwargs: list[dict[str, Any]]
|
||||
number_channels: int
|
||||
enable_channel_interleave: bool
|
||||
attention_implementation: str
|
||||
compute_dtype: str
|
||||
codec_weight_dtype: str
|
||||
quantizer_type: str
|
||||
quantizer_kwargs: dict[str, Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str | None = None,
|
||||
sampling_rate: int = 48000,
|
||||
downsample_rate: int = 3840,
|
||||
causal_transformer_context_duration: float = 10.0,
|
||||
encoder_kwargs: list[dict[str, Any]] | None = None,
|
||||
decoder_kwargs: list[dict[str, Any]] | None = None,
|
||||
number_channels: int = 2,
|
||||
enable_channel_interleave: bool = True,
|
||||
attention_implementation: str = "sdpa",
|
||||
compute_dtype: str = "fp32",
|
||||
codec_weight_dtype: str = "fp32",
|
||||
quantizer_type: str = "rlfq",
|
||||
quantizer_kwargs: dict[str, Any] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
# Some checkpoints might include an incorrect/legacy `model_type` (e.g. "speech_tokenizer").
|
||||
# We drop it to avoid overriding the class-level `model_type`.
|
||||
kwargs.pop("model_type", None)
|
||||
if "channels_numbers" in kwargs:
|
||||
number_channels = kwargs.pop("channels_numbers")
|
||||
if "enable_channel_interleave" in kwargs:
|
||||
enable_channel_interleave = kwargs.pop("enable_channel_interleave")
|
||||
if "attention_backend" in kwargs and attention_implementation == "sdpa":
|
||||
attention_implementation = kwargs.pop("attention_backend")
|
||||
if "codec_compute_dtype" in kwargs and compute_dtype == "fp32":
|
||||
compute_dtype = kwargs.pop("codec_compute_dtype")
|
||||
if "codec_load_dtype" in kwargs and codec_weight_dtype == "fp32":
|
||||
codec_weight_dtype = kwargs.pop("codec_load_dtype")
|
||||
|
||||
# `version` is accepted for compatibility but not used in modeling.
|
||||
self.version = version
|
||||
self.sampling_rate = sampling_rate
|
||||
self.downsample_rate = downsample_rate
|
||||
self.causal_transformer_context_duration = causal_transformer_context_duration
|
||||
self.number_channels = number_channels
|
||||
self.enable_channel_interleave = enable_channel_interleave
|
||||
self.attention_implementation = attention_implementation
|
||||
self.compute_dtype = compute_dtype
|
||||
self.codec_weight_dtype = codec_weight_dtype
|
||||
# Default encoder configuration
|
||||
if encoder_kwargs is None:
|
||||
encoder_kwargs = [
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 240,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 240,
|
||||
"output_dimension": 384,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 768,
|
||||
"output_dimension": 384,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 768,
|
||||
"output_dimension": 640,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 1280,
|
||||
"output_dimension": 768,
|
||||
"d_model": 1280,
|
||||
"num_heads": 20,
|
||||
"num_layers": 32,
|
||||
"dim_feedforward": 5120,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
]
|
||||
self.encoder_kwargs = encoder_kwargs
|
||||
|
||||
# Default decoder configuration (execution order)
|
||||
if decoder_kwargs is None:
|
||||
decoder_kwargs = [
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 768,
|
||||
"output_dimension": 1280,
|
||||
"d_model": 1280,
|
||||
"num_heads": 20,
|
||||
"num_layers": 32,
|
||||
"dim_feedforward": 5120,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 640,
|
||||
"output_dimension": 768,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 384,
|
||||
"output_dimension": 768,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 384,
|
||||
"output_dimension": 768,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 2,
|
||||
},
|
||||
{
|
||||
"module_type": "Transformer",
|
||||
"input_dimension": 384,
|
||||
"output_dimension": 240,
|
||||
"d_model": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"dim_feedforward": 3072,
|
||||
"causal": True,
|
||||
"norm": "layer_norm",
|
||||
"positional_embedding": "rope",
|
||||
"max_period": 10000,
|
||||
"gating": "none",
|
||||
"layer_scale": 0.01,
|
||||
"conv_layout": True,
|
||||
},
|
||||
{
|
||||
"module_type": "PatchedPretransform",
|
||||
"patch_size": 240,
|
||||
},
|
||||
]
|
||||
self.decoder_kwargs = decoder_kwargs
|
||||
|
||||
# Default quantizer configuration
|
||||
if quantizer_kwargs is None:
|
||||
quantizer_kwargs = {
|
||||
"input_dim": 768,
|
||||
"rvq_dim": 512,
|
||||
"output_dim": 768,
|
||||
"num_quantizers": 32,
|
||||
"codebook_size": 1024,
|
||||
"codebook_dim": 8,
|
||||
"quantizer_type": "rlfq",
|
||||
}
|
||||
|
||||
# Handle quantizer_type from kwargs or config
|
||||
kw_qtype = quantizer_kwargs.get("quantizer_type", None)
|
||||
if kw_qtype is not None:
|
||||
self.quantizer_type = kw_qtype
|
||||
else:
|
||||
self.quantizer_type = quantizer_type
|
||||
quantizer_kwargs["quantizer_type"] = quantizer_type
|
||||
|
||||
self.quantizer_kwargs = quantizer_kwargs
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@property
|
||||
def num_quantizers(self) -> int:
|
||||
"""Return the number of quantizers from quantizer_kwargs."""
|
||||
return self.quantizer_kwargs.get("num_quantizers", 32)
|
||||
|
||||
@property
|
||||
def codebook_size(self) -> int:
|
||||
"""Return the codebook size from quantizer_kwargs."""
|
||||
return self.quantizer_kwargs.get("codebook_size", 4096)
|
||||
|
||||
@property
|
||||
def frame_rate(self) -> float:
|
||||
"""Return the frame rate (tokens per second)."""
|
||||
return self.sampling_rate / self.downsample_rate
|
||||
|
||||
|
||||
__all__ = ["MossAudioTokenizerConfig"]
|
||||
@@ -0,0 +1,164 @@
|
||||
# Copyright 2026 OpenMOSS and the vLLM-Omni team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
"""CUDA graph wrapper for MOSS-TTS Local streaming codec decode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.cuda import CUDAGraph
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _CapturedStreamingDecodeGraph:
|
||||
graph: CUDAGraph
|
||||
static_codes: torch.Tensor
|
||||
static_lengths: torch.Tensor
|
||||
static_audio: torch.Tensor
|
||||
static_audio_lengths: torch.Tensor
|
||||
|
||||
|
||||
class CUDAGraphStreamingDecoderWrapper:
|
||||
"""Replay fixed-size streaming ``_decode_frame`` steps with CUDA graphs.
|
||||
|
||||
The wrapped codec must already be inside ``codec.streaming(batch_size)``.
|
||||
Graphs are keyed by the streaming step frame count ``T``. The batch width is
|
||||
fixed to the codec stream slot count.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
codec: nn.Module,
|
||||
*,
|
||||
batch_size: int,
|
||||
num_quantizers: int,
|
||||
reset_streaming_state: Callable[[], None] | None = None,
|
||||
) -> None:
|
||||
self.codec = codec
|
||||
self.batch_size = int(batch_size)
|
||||
self.num_quantizers = int(num_quantizers)
|
||||
self.reset_streaming_state = reset_streaming_state
|
||||
self.graphs: dict[int, _CapturedStreamingDecodeGraph] = {}
|
||||
self._pool = None
|
||||
self._warmed_up = False
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
return bool(self.graphs)
|
||||
|
||||
@property
|
||||
def capture_sizes(self) -> list[int]:
|
||||
return sorted(self.graphs)
|
||||
|
||||
@torch.no_grad()
|
||||
def warmup(self, device: torch.device, capture_sizes: list[int]) -> None:
|
||||
if self._warmed_up:
|
||||
return
|
||||
self._warmed_up = True
|
||||
if device.type != "cuda":
|
||||
return
|
||||
sizes = sorted({int(size) for size in capture_sizes if int(size) > 0}, reverse=True)
|
||||
if not sizes:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"MOSS-TTS streaming decoder CUDA graph warmup: batch_size=%d n_vq=%d capture_sizes=%s",
|
||||
self.batch_size,
|
||||
self.num_quantizers,
|
||||
list(reversed(sizes)),
|
||||
)
|
||||
start_s = time.perf_counter()
|
||||
with torch.cuda.device(device):
|
||||
for size in sizes:
|
||||
try:
|
||||
self._capture(size, device)
|
||||
logger.info(" Captured MOSS-TTS streaming decoder CUDA graph for size=%d", size)
|
||||
except Exception:
|
||||
self.graphs.pop(size, None)
|
||||
logger.warning(
|
||||
" Failed to capture MOSS-TTS streaming decoder CUDA graph for size=%d; "
|
||||
"this size will use eager decode",
|
||||
size,
|
||||
exc_info=True,
|
||||
)
|
||||
logger.info(
|
||||
"MOSS-TTS streaming decoder CUDA graph warmup complete: %d/%d captured in %.1f ms",
|
||||
len(self.graphs),
|
||||
len(sizes),
|
||||
(time.perf_counter() - start_s) * 1000.0,
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def _capture(self, size: int, device: torch.device) -> None:
|
||||
codes = torch.zeros(
|
||||
self.num_quantizers,
|
||||
self.batch_size,
|
||||
size,
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
)
|
||||
lengths = torch.full((self.batch_size,), size, dtype=torch.long, device=device)
|
||||
exec_mask = torch.ones(self.batch_size, dtype=torch.bool, device=device)
|
||||
|
||||
self.codec._set_streaming_exec_mask(exec_mask)
|
||||
stream = torch.cuda.Stream()
|
||||
stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(stream):
|
||||
_ = self.codec._decode_frame(codes, lengths)
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
torch.accelerator.synchronize(device)
|
||||
|
||||
if self.reset_streaming_state is not None:
|
||||
self.reset_streaming_state()
|
||||
self.codec._set_streaming_exec_mask(exec_mask)
|
||||
if self._pool is None:
|
||||
self._pool = current_platform.get_global_graph_pool()
|
||||
graph = CUDAGraph()
|
||||
with torch.cuda.graph(graph, pool=self._pool, capture_error_mode="thread_local"):
|
||||
output = self.codec._decode_frame(codes, lengths)
|
||||
|
||||
if output.audio is None or output.audio_lengths is None:
|
||||
raise RuntimeError("MOSS-TTS streaming decoder CUDA graph capture produced empty audio.")
|
||||
|
||||
self.graphs[size] = _CapturedStreamingDecodeGraph(
|
||||
graph=graph,
|
||||
static_codes=codes,
|
||||
static_lengths=lengths,
|
||||
static_audio=output.audio,
|
||||
static_audio_lengths=output.audio_lengths,
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def decode(
|
||||
self,
|
||||
codes_step: torch.Tensor,
|
||||
exec_mask: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor] | None:
|
||||
if not codes_step.is_cuda or torch.cuda.is_current_stream_capturing():
|
||||
return None
|
||||
n_vq, batch_size, step_t = codes_step.shape
|
||||
if int(n_vq) != self.num_quantizers or int(batch_size) != self.batch_size:
|
||||
return None
|
||||
# The streaming codec advances its internal KV offsets by tensor T, so
|
||||
# bucket-padding a smaller step into a larger captured graph would write
|
||||
# padded frames into state. Capture exact step sizes instead.
|
||||
entry = self.graphs.get(int(step_t))
|
||||
if entry is None:
|
||||
return None
|
||||
|
||||
self.codec._set_streaming_exec_mask(exec_mask)
|
||||
entry.static_codes.copy_(codes_step)
|
||||
entry.graph.replay()
|
||||
return entry.static_audio, entry.static_audio_lengths
|
||||
|
||||
|
||||
__all__ = ["CUDAGraphStreamingDecoderWrapper"]
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
@@ -19,6 +20,15 @@ from vllm_omni.model_executor.models.moss_tts.audio_tokenizer import (
|
||||
MossAudioTokenizerConfig,
|
||||
MossAudioTokenizerModel,
|
||||
)
|
||||
from vllm_omni.model_executor.models.moss_tts.audio_tokenizer_v2 import (
|
||||
MossAudioTokenizerModel as MossAudioTokenizerV2Model,
|
||||
)
|
||||
from vllm_omni.model_executor.models.moss_tts.configuration_moss_audio_tokenizer_v2 import (
|
||||
MossAudioTokenizerConfig as MossAudioTokenizerV2Config,
|
||||
)
|
||||
from vllm_omni.model_executor.models.moss_tts.cuda_graph_streaming_decoder_wrapper import (
|
||||
CUDAGraphStreamingDecoderWrapper,
|
||||
)
|
||||
from vllm_omni.model_executor.models.moss_tts.moss_codec_cudagraph import (
|
||||
MossTTSCUDAGraphCodecWrapper,
|
||||
)
|
||||
@@ -27,6 +37,121 @@ from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class _MossCodecStreamSession:
|
||||
"""Persistent streaming decode session for vendored MOSS-Audio-Tokenizer-v2."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
codec: nn.Module,
|
||||
*,
|
||||
stream_slots: int,
|
||||
n_vq: int,
|
||||
cudagraph_capture_sizes: list[int] | None = None,
|
||||
) -> None:
|
||||
self._codec = codec
|
||||
self._stream_slots = int(stream_slots)
|
||||
self._batch_size = self._stream_slots
|
||||
self._n_vq = int(n_vq)
|
||||
self._device = next(codec.parameters()).device
|
||||
self._free_stream_slots = list(range(self._stream_slots))
|
||||
self._exit_stack = contextlib.ExitStack()
|
||||
self._closed = False
|
||||
with torch.no_grad():
|
||||
self._exit_stack.enter_context(codec.streaming(self._batch_size))
|
||||
self._cudagraph_wrapper: CUDAGraphStreamingDecoderWrapper | None = None
|
||||
capture_sizes = sorted({int(size) for size in (cudagraph_capture_sizes or []) if int(size) > 0})
|
||||
if capture_sizes and self._device.type == "cuda":
|
||||
self._cudagraph_wrapper = CUDAGraphStreamingDecoderWrapper(
|
||||
codec,
|
||||
batch_size=self._batch_size,
|
||||
num_quantizers=self._n_vq,
|
||||
reset_streaming_state=lambda: self.reset_slots(list(range(self._batch_size))),
|
||||
)
|
||||
self._cudagraph_wrapper.warmup(self._device, capture_sizes)
|
||||
self.reset_slots(list(range(self._batch_size)))
|
||||
if not self._cudagraph_wrapper.is_ready:
|
||||
self._cudagraph_wrapper = None
|
||||
|
||||
def acquire(self) -> int | None:
|
||||
if not self._free_stream_slots:
|
||||
return None
|
||||
return self._free_stream_slots.pop()
|
||||
|
||||
def release(self, slot: int) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self.reset_slots([slot])
|
||||
self._free_stream_slots.append(slot)
|
||||
|
||||
def reset_slots(self, slots: list[int]) -> None:
|
||||
if not slots:
|
||||
return
|
||||
reset_mask = torch.zeros(self._batch_size, dtype=torch.bool, device=self._device)
|
||||
reset_mask[slots] = True
|
||||
|
||||
def _reset(module: nn.Module) -> None:
|
||||
state = getattr(module, "_streaming_state", None)
|
||||
if state is not None:
|
||||
state.reset(reset_mask.to(state.device))
|
||||
|
||||
with torch.no_grad():
|
||||
self._codec.apply(_reset)
|
||||
|
||||
def close(self) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
with torch.no_grad():
|
||||
self._exit_stack.close()
|
||||
self._closed = True
|
||||
|
||||
@torch.no_grad()
|
||||
def step(self, slot_codes: dict[int, torch.Tensor]) -> dict[int, torch.Tensor]:
|
||||
if not slot_codes:
|
||||
return {}
|
||||
step_lengths = {int(codes.shape[1]) for codes in slot_codes.values()}
|
||||
if len(step_lengths) != 1:
|
||||
raise ValueError(f"MOSS codec streaming step needs uniform T, got {sorted(step_lengths)}")
|
||||
(step_t,) = step_lengths
|
||||
codes_step = torch.zeros(
|
||||
self._n_vq,
|
||||
self._batch_size,
|
||||
step_t,
|
||||
dtype=torch.long,
|
||||
device=self._device,
|
||||
)
|
||||
codes_lengths = torch.zeros(self._batch_size, dtype=torch.long, device=self._device)
|
||||
exec_mask = torch.zeros(self._batch_size, dtype=torch.bool, device=self._device)
|
||||
for slot, codes in slot_codes.items():
|
||||
codes_step[:, slot, :] = codes.to(device=self._device, dtype=torch.long)
|
||||
codes_lengths[slot] = int(codes.shape[1])
|
||||
exec_mask[slot] = True
|
||||
|
||||
graph_output: tuple[torch.Tensor, torch.Tensor] | None = None
|
||||
if self._cudagraph_wrapper is not None:
|
||||
graph_output = self._cudagraph_wrapper.decode(codes_step, exec_mask)
|
||||
|
||||
used_cudagraph = graph_output is not None
|
||||
if used_cudagraph:
|
||||
audio_tensor, lengths_tensor = graph_output
|
||||
else:
|
||||
self._codec._set_streaming_exec_mask(exec_mask)
|
||||
result = self._codec._decode_frame(codes_step, codes_lengths)
|
||||
if result.audio is None:
|
||||
return {}
|
||||
audio_tensor = result.audio
|
||||
lengths_tensor = result.audio_lengths
|
||||
|
||||
audio = audio_tensor.detach().to("cpu", torch.float32)
|
||||
lengths = lengths_tensor.detach().to("cpu") if lengths_tensor is not None else None
|
||||
out: dict[int, torch.Tensor] = {}
|
||||
for slot in slot_codes:
|
||||
wav = audio[slot]
|
||||
if lengths is not None:
|
||||
wav = wav[..., : int(lengths[slot].item())]
|
||||
out[slot] = wav.contiguous()
|
||||
return out
|
||||
|
||||
|
||||
class MossTTSCodecDecoder(nn.Module):
|
||||
"""Stage-1 decoder for all MOSS-TTS variants.
|
||||
|
||||
@@ -75,6 +200,19 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
self._cuda_graph_wrapper: MossTTSCUDAGraphCodecWrapper | None = None
|
||||
self._n_channels: int = 1
|
||||
self._sr_tensor = torch.tensor(self._OUTPUT_SAMPLE_RATE, dtype=torch.int32)
|
||||
self._stream_session: _MossCodecStreamSession | None = None
|
||||
self._stream_slots: int = self._connector_int("codec_stream_slots", default=0)
|
||||
self._stream_chunk_frames: int = self._connector_int("codec_chunk_frames", default=0)
|
||||
self._stream_max_step_frames: int = self._stream_chunk_frames or 100
|
||||
self._stream_req_slots: dict[str, int] = {}
|
||||
self._stream_pending_codes: dict[str, list[torch.Tensor]] = {}
|
||||
self._stream_starved_reqs: set[str] = set()
|
||||
self._codec_streaming: bool = self._connector_bool("codec_streaming", default=False)
|
||||
default_graph_capture_max = self._stream_chunk_frames or min(self._stream_max_step_frames, 32)
|
||||
default_graph_capture_sizes = list(range(1, max(1, default_graph_capture_max) + 1))
|
||||
self._streaming_cudagraph_capture_sizes = self._streaming_cudagraph_capture_sizes_from_compilation_config(
|
||||
default_graph_capture_sizes
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# vLLM-Omni stubs (codec has no AR loop)
|
||||
@@ -112,9 +250,9 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
chunk transfer adapter assigns those to ``request.prompt_token_ids``,
|
||||
which arrives here as ``input_ids`` concatenated across all requests.
|
||||
Per-request slice boundaries are computed from
|
||||
``kwargs["num_scheduled_tokens"]`` (a list of token counts, one per
|
||||
request). ``runtime_additional_information`` carries per-request
|
||||
metadata such as ``left_context_size``.
|
||||
``kwargs["seq_token_counts"]`` (token counts, one per request).
|
||||
``runtime_additional_information`` carries per-request metadata such as
|
||||
``left_context_size``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -123,8 +261,8 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
multimodal_outputs["sr"] — list of scalar int32 tensors
|
||||
"""
|
||||
sr_tensor = self._sr_tensor
|
||||
empty = torch.zeros((0,), dtype=torch.float32)
|
||||
info_list: list[dict[str, Any]] = runtime_additional_information or [{}]
|
||||
empty = self._empty_audio()
|
||||
info_list: list[dict[str, Any]] = list(runtime_additional_information or [{}])
|
||||
num_req = max(len(info_list), 1)
|
||||
|
||||
if self._codec is None:
|
||||
@@ -137,27 +275,59 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
},
|
||||
)
|
||||
|
||||
if self._codec_streaming and runtime_additional_information is None:
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={
|
||||
"model_outputs": [empty] * num_req,
|
||||
"sr": [sr_tensor] * num_req,
|
||||
},
|
||||
)
|
||||
|
||||
audios: list[torch.Tensor] = [empty] * num_req
|
||||
srs: list[torch.Tensor] = [sr_tensor] * num_req
|
||||
device = next(self._codec.parameters()).device
|
||||
streaming_work: list[tuple[int, str, torch.Tensor, bool]] = []
|
||||
|
||||
if input_ids is None or input_ids.numel() == 0:
|
||||
for i, wav in self._finish_empty_streaming_requests(info_list).items():
|
||||
audios[i] = wav.reshape(-1) if wav.ndim == 1 or int(wav.shape[0]) == 1 else wav
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": audios, "sr": srs},
|
||||
)
|
||||
|
||||
# ``input_ids`` is concatenated across all requests; split by
|
||||
# query_start_loc-style offsets from ``num_scheduled_tokens`` in
|
||||
# kwargs if available, else assume one request.
|
||||
# ``input_ids`` is concatenated across all requests. vLLM-Omni runners
|
||||
# pass the per-request lengths via the shared code2wav contract
|
||||
# ``seq_token_counts``.
|
||||
ids_flat = input_ids.reshape(-1).to(dtype=torch.long)
|
||||
num_scheduled_tokens = kwargs.get("num_scheduled_tokens")
|
||||
if isinstance(num_scheduled_tokens, (list, tuple)) and len(num_scheduled_tokens) == num_req:
|
||||
offsets = [0]
|
||||
for n in num_scheduled_tokens:
|
||||
offsets.append(offsets[-1] + int(n))
|
||||
else:
|
||||
offsets = [0, int(ids_flat.shape[0])]
|
||||
token_counts = self._normalize_seq_token_counts(kwargs.get("seq_token_counts"))
|
||||
if token_counts is None:
|
||||
raise RuntimeError(
|
||||
"MossTTS codec requires seq_token_counts; otherwise concatenated "
|
||||
"codec tokens cannot be split per request."
|
||||
)
|
||||
if sum(token_counts) != int(ids_flat.shape[0]):
|
||||
raise RuntimeError(
|
||||
"MossTTS codec seq_token_counts mismatch: "
|
||||
f"counts={token_counts}, sum={sum(token_counts)}, input_tokens={int(ids_flat.shape[0])}."
|
||||
)
|
||||
|
||||
num_req = len(token_counts)
|
||||
if len(info_list) < num_req:
|
||||
info_list.extend({} for _ in range(num_req - len(info_list)))
|
||||
elif len(info_list) > num_req:
|
||||
info_list = info_list[:num_req]
|
||||
if len(audios) < num_req:
|
||||
audios.extend(empty for _ in range(num_req - len(audios)))
|
||||
srs.extend(sr_tensor for _ in range(num_req - len(srs)))
|
||||
elif len(audios) > num_req:
|
||||
audios = audios[:num_req]
|
||||
srs = srs[:num_req]
|
||||
|
||||
offsets = [0]
|
||||
for n in token_counts:
|
||||
offsets.append(offsets[-1] + int(n))
|
||||
|
||||
for i, info in enumerate(info_list):
|
||||
if i + 1 >= len(offsets):
|
||||
@@ -165,6 +335,14 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
seg = ids_flat[offsets[i] : offsets[i + 1]]
|
||||
if seg.numel() == 0:
|
||||
continue
|
||||
meta = (info.get("meta", {}) if isinstance(info, dict) else {}) or {}
|
||||
finished = bool(meta.get("stream_finished", meta.get("finished", False)))
|
||||
streaming_enabled = bool(meta.get("codec_streaming", self._codec_streaming))
|
||||
code_flat_numel = meta.get("code_flat_numel")
|
||||
if streaming_enabled and finished and code_flat_numel is not None and int(code_flat_numel) == 0:
|
||||
for _, wav in self._finish_empty_streaming_requests([info]).items():
|
||||
audios[i] = wav.reshape(-1) if wav.ndim == 1 or int(wav.shape[0]) == 1 else wav
|
||||
continue
|
||||
if seg.numel() % self._n_vq != 0:
|
||||
logger.warning(
|
||||
"MossTTS codec input length %d not divisible by n_vq %d; skipping.",
|
||||
@@ -181,14 +359,19 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
codebook_size = self._codec.config.codebook_size
|
||||
codes_nq_t = codes_nq_t.clamp_(0, int(codebook_size) - 1)
|
||||
|
||||
meta = (info.get("meta", {}) if isinstance(info, dict) else {}) or {}
|
||||
left_ctx = self._runtime_value(info, meta, "left_context_size", 0)
|
||||
left_ctx = meta.get("left_context_size", 0)
|
||||
if isinstance(left_ctx, (list, tuple)):
|
||||
left_ctx = int(left_ctx[0]) if left_ctx else 0
|
||||
elif isinstance(left_ctx, torch.Tensor):
|
||||
left_ctx = int(left_ctx.reshape(-1)[0].item()) if left_ctx.numel() else 0
|
||||
left_ctx = int(left_ctx)
|
||||
|
||||
req_key = self._runtime_request_key(info, meta, i)
|
||||
|
||||
if streaming_enabled:
|
||||
streaming_work.append((i, req_key, codes_nq_t, finished))
|
||||
continue
|
||||
|
||||
if self._cuda_graph_wrapper is not None:
|
||||
out = self._cuda_graph_wrapper.decode(codes_nq_t)
|
||||
else:
|
||||
@@ -216,21 +399,273 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
)
|
||||
wav = wav[..., trim:]
|
||||
|
||||
audios[i] = wav.reshape(-1) if self._n_channels == 1 else wav
|
||||
audios[i] = wav.reshape(-1) if wav.ndim == 1 or int(wav.shape[0]) == 1 else wav
|
||||
|
||||
if streaming_work:
|
||||
for i, wav in self._decode_streaming_batch(streaming_work).items():
|
||||
audios[i] = wav.reshape(-1) if wav.ndim == 1 or int(wav.shape[0]) == 1 else wav
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": audios, "sr": srs},
|
||||
)
|
||||
|
||||
def _finish_empty_streaming_requests(self, info_list: list[dict[str, Any]]) -> dict[int, torch.Tensor]:
|
||||
"""Release codec stream state for empty finish sentinels.
|
||||
|
||||
Stage-0 can finish on a step that emits no new audio frame. The stage
|
||||
input processor forwards that as an empty payload with finished=true.
|
||||
If a request never acquired a stream slot, do not offline-decode its
|
||||
buffered codes here: streaming requests must only emit client deltas.
|
||||
"""
|
||||
session = self._stream_session
|
||||
outputs: dict[int, torch.Tensor] = {}
|
||||
if session is None:
|
||||
return outputs
|
||||
for i, info in enumerate(info_list):
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
meta = (info.get("meta", {}) or {}) if isinstance(info.get("meta", {}), dict) else {}
|
||||
if not bool(meta.get("codec_streaming", self._codec_streaming)):
|
||||
continue
|
||||
finished = bool(meta.get("stream_finished", meta.get("finished", False)))
|
||||
if not finished:
|
||||
continue
|
||||
req_key = self._runtime_request_key(info, meta, i)
|
||||
slot = self._stream_req_slots.get(req_key)
|
||||
pending = req_key in self._stream_pending_codes
|
||||
if slot is not None or pending:
|
||||
if pending and slot is None:
|
||||
logger.warning(
|
||||
"MOSS codec stream request %s finished before a codec stream slot became available; "
|
||||
"dropping buffered codes instead of offline-decoding a non-delta waveform.",
|
||||
req_key,
|
||||
)
|
||||
self._finish_stream_request(req_key, session, slot)
|
||||
return outputs
|
||||
|
||||
@staticmethod
|
||||
def _runtime_value(info: Any, meta: dict[str, Any], name: str, default: Any = None) -> Any:
|
||||
if name in meta:
|
||||
return meta[name]
|
||||
if isinstance(info, dict) and name in info:
|
||||
return info[name]
|
||||
def _normalize_seq_token_counts(value: Any) -> list[int] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise TypeError(
|
||||
"MossTTS codec expects seq_token_counts to be a list/tuple of per-request token counts, "
|
||||
f"got {type(value).__name__}."
|
||||
)
|
||||
counts = [int(item) for item in value]
|
||||
if not counts:
|
||||
return None
|
||||
for count in counts:
|
||||
if count < 0:
|
||||
raise ValueError(f"MossTTS codec seq_token_counts must be non-negative, got {counts}.")
|
||||
return counts
|
||||
|
||||
def _runtime_request_key(self, info: Any, meta: dict[str, Any], index: int) -> str:
|
||||
for value in (
|
||||
meta.get("req_id"),
|
||||
info.get("request_id") if isinstance(info, dict) else None,
|
||||
):
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = value[0] if value else None
|
||||
if value is not None:
|
||||
return str(value)
|
||||
return f"moss-codec-stream-{index}"
|
||||
|
||||
def _empty_audio(self) -> torch.Tensor:
|
||||
if self._n_channels > 1:
|
||||
return torch.zeros((self._n_channels, 0), dtype=torch.float32)
|
||||
return torch.zeros((0,), dtype=torch.float32)
|
||||
|
||||
def _ensure_stream_session(self) -> _MossCodecStreamSession | None:
|
||||
if self._codec is None:
|
||||
return None
|
||||
if self._stream_session is not None:
|
||||
return self._stream_session
|
||||
slots = self._stream_slots
|
||||
if slots <= 0:
|
||||
scheduler_cfg = getattr(self.vllm_config, "scheduler_config", None)
|
||||
slots = int(getattr(scheduler_cfg, "max_num_seqs", 1) or 1)
|
||||
self._stream_session = _MossCodecStreamSession(
|
||||
self._codec,
|
||||
stream_slots=max(1, slots),
|
||||
n_vq=self._n_vq,
|
||||
cudagraph_capture_sizes=self._streaming_cudagraph_capture_sizes,
|
||||
)
|
||||
return self._stream_session
|
||||
|
||||
def _decode_streaming_batch(
|
||||
self,
|
||||
items: list[tuple[int, str, torch.Tensor, bool]],
|
||||
) -> dict[int, torch.Tensor]:
|
||||
session = self._ensure_stream_session()
|
||||
if session is None:
|
||||
return {}
|
||||
|
||||
outputs: dict[int, torch.Tensor] = {}
|
||||
grouped: dict[int, list[tuple[int, str, int, torch.Tensor, bool]]] = {}
|
||||
max_step_frames = max(1, int(self._stream_max_step_frames))
|
||||
|
||||
for output_index, request_id, codes_nq_t, finished in items:
|
||||
pending = self._stream_pending_codes.get(request_id)
|
||||
slot = self._stream_req_slots.get(request_id)
|
||||
if slot is None:
|
||||
slot = session.acquire()
|
||||
if slot is None:
|
||||
self._append_stream_pending(request_id, codes_nq_t)
|
||||
if request_id not in self._stream_starved_reqs:
|
||||
logger.warning(
|
||||
"MOSS codec streaming slots exhausted; buffering %s until a stream slot is available.",
|
||||
request_id,
|
||||
)
|
||||
self._stream_starved_reqs.add(request_id)
|
||||
if finished:
|
||||
logger.warning(
|
||||
"MOSS codec stream request %s finished before a codec stream slot became available; "
|
||||
"dropping buffered codes instead of offline-decoding a non-delta waveform.",
|
||||
request_id,
|
||||
)
|
||||
self._finish_stream_request(request_id, session, None)
|
||||
continue
|
||||
self._stream_req_slots[request_id] = slot
|
||||
|
||||
if pending:
|
||||
self._append_stream_pending(request_id, codes_nq_t)
|
||||
replay_codes = self._pop_stream_pending(request_id)
|
||||
wav = self._decode_stream_slot_sequence(session, slot, replay_codes)
|
||||
if wav is not None:
|
||||
outputs[output_index] = wav
|
||||
if finished:
|
||||
self._finish_stream_request(request_id, session, slot)
|
||||
continue
|
||||
|
||||
if int(codes_nq_t.shape[1]) > max_step_frames:
|
||||
wav = self._decode_stream_slot_sequence(session, slot, codes_nq_t)
|
||||
if wav is not None:
|
||||
outputs[output_index] = wav
|
||||
if finished:
|
||||
self._finish_stream_request(request_id, session, slot)
|
||||
continue
|
||||
|
||||
grouped.setdefault(int(codes_nq_t.shape[1]), []).append(
|
||||
(output_index, request_id, slot, codes_nq_t, finished)
|
||||
)
|
||||
|
||||
for group in grouped.values():
|
||||
plan = {slot: codes_nq_t for _, _, slot, codes_nq_t, _ in group}
|
||||
decoded = session.step(plan)
|
||||
for output_index, request_id, slot, _, finished in group:
|
||||
wav = decoded.get(slot)
|
||||
if wav is not None:
|
||||
outputs[output_index] = wav
|
||||
if finished:
|
||||
self._finish_stream_request(request_id, session, slot)
|
||||
|
||||
return outputs
|
||||
|
||||
def _append_stream_pending(self, request_id: str, codes_nq_t: torch.Tensor) -> None:
|
||||
self._stream_pending_codes.setdefault(request_id, []).append(
|
||||
codes_nq_t.detach().to("cpu", torch.long).contiguous()
|
||||
)
|
||||
|
||||
def _pop_stream_pending(self, request_id: str) -> torch.Tensor:
|
||||
pending = self._stream_pending_codes.pop(request_id, [])
|
||||
if not pending:
|
||||
return torch.empty((self._n_vq, 0), dtype=torch.long)
|
||||
return torch.cat(pending, dim=1).contiguous()
|
||||
|
||||
def _decode_stream_slot_sequence(
|
||||
self,
|
||||
session: _MossCodecStreamSession,
|
||||
slot: int,
|
||||
codes_nq_t: torch.Tensor,
|
||||
) -> torch.Tensor | None:
|
||||
if codes_nq_t.numel() == 0:
|
||||
return None
|
||||
max_step_frames = max(1, int(self._stream_max_step_frames))
|
||||
parts: list[torch.Tensor] = []
|
||||
for start in range(0, int(codes_nq_t.shape[1]), max_step_frames):
|
||||
chunk = codes_nq_t[:, start : start + max_step_frames]
|
||||
decoded = session.step({slot: chunk})
|
||||
wav = decoded.get(slot)
|
||||
if wav is not None:
|
||||
parts.append(wav)
|
||||
if not parts:
|
||||
return None
|
||||
return torch.cat(parts, dim=-1) if len(parts) > 1 else parts[0]
|
||||
|
||||
def _finish_stream_request(
|
||||
self,
|
||||
request_id: str,
|
||||
session: _MossCodecStreamSession,
|
||||
slot: int | None,
|
||||
) -> None:
|
||||
if slot is not None:
|
||||
session.release(slot)
|
||||
self._stream_req_slots.pop(request_id, None)
|
||||
self._stream_pending_codes.pop(request_id, None)
|
||||
self._stream_starved_reqs.discard(request_id)
|
||||
|
||||
def on_requests_finished(self, finished_req_ids: set[str] | list[str]) -> None:
|
||||
"""Release codec streaming slots when requests finish outside payload flow.
|
||||
|
||||
Normal streaming completion releases slots from ``_decode_streaming_batch``
|
||||
when the Stage-0 payload carries ``finished=True``. Client disconnects
|
||||
and engine-side aborts can finish a request without delivering that
|
||||
terminal payload, so the runner calls this hook from its finished-request
|
||||
path to avoid leaking stream slots and buffered codes.
|
||||
"""
|
||||
session = self._stream_session
|
||||
for req_id in finished_req_ids:
|
||||
request_id = str(req_id)
|
||||
slot = self._stream_req_slots.get(request_id)
|
||||
has_state = (
|
||||
slot is not None or request_id in self._stream_pending_codes or request_id in self._stream_starved_reqs
|
||||
)
|
||||
if not has_state:
|
||||
continue
|
||||
if session is not None:
|
||||
self._finish_stream_request(request_id, session, slot)
|
||||
else:
|
||||
self._stream_req_slots.pop(request_id, None)
|
||||
self._stream_pending_codes.pop(request_id, None)
|
||||
self._stream_starved_reqs.discard(request_id)
|
||||
|
||||
def _connector_int(self, name: str, default: int = 0) -> int:
|
||||
model_cfg = getattr(self.vllm_config, "model_config", None)
|
||||
connector_cfg = getattr(model_cfg, "stage_connector_config", None)
|
||||
if isinstance(connector_cfg, dict):
|
||||
extra_cfg: dict | None = connector_cfg.get("extra", connector_cfg)
|
||||
else:
|
||||
extra_cfg = getattr(connector_cfg, "extra", None)
|
||||
if isinstance(extra_cfg, dict) and name in extra_cfg:
|
||||
return int(extra_cfg[name])
|
||||
return default
|
||||
|
||||
def _connector_bool(self, name: str, default: bool = False) -> bool:
|
||||
model_cfg = getattr(self.vllm_config, "model_config", None)
|
||||
connector_cfg = getattr(model_cfg, "stage_connector_config", None)
|
||||
if isinstance(connector_cfg, dict):
|
||||
extra_cfg: dict | None = connector_cfg.get("extra", connector_cfg)
|
||||
else:
|
||||
extra_cfg = getattr(connector_cfg, "extra", None)
|
||||
if isinstance(extra_cfg, dict) and name in extra_cfg:
|
||||
return bool(extra_cfg[name])
|
||||
return default
|
||||
|
||||
def _streaming_cudagraph_capture_sizes_from_compilation_config(
|
||||
self,
|
||||
default: list[int],
|
||||
) -> list[int]:
|
||||
if getattr(self.vllm_config.model_config, "enforce_eager", True):
|
||||
return []
|
||||
compilation_config = getattr(self.vllm_config, "compilation_config", None)
|
||||
capture_sizes = getattr(compilation_config, "cudagraph_capture_sizes", None)
|
||||
if capture_sizes:
|
||||
max_step_frames = max(1, int(self._stream_max_step_frames))
|
||||
return sorted({int(size) for size in capture_sizes if 0 < int(size) <= max_step_frames})
|
||||
return list(default)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Weight loading
|
||||
# ------------------------------------------------------------------
|
||||
@@ -272,14 +707,12 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
(".self_attn.out_projs.0.", ".attn.out_proj."),
|
||||
(".linear1.", ".ff1."),
|
||||
(".linear2.", ".ff2."),
|
||||
# v2 (MOSS-Audio-Tokenizer-v2) uses different submodule names for
|
||||
# the same attention/FFN sublayers — no trailing "s"/index on
|
||||
# in_proj/out_proj, and an `ffn.{0,2}` Sequential instead of
|
||||
# separate linear1/linear2 attributes.
|
||||
(".self_attn.in_proj.", ".attn.in_proj."),
|
||||
(".self_attn.out_proj.", ".attn.out_proj."),
|
||||
(".ffn.0.", ".ff1."),
|
||||
(".ffn.2.", ".ff2."),
|
||||
# v2 checkpoint names use singular in_proj/out_proj and ffn.{0,2};
|
||||
# the vendored module keeps the original MOSS layer names.
|
||||
(".self_attn.in_proj.", ".self_attn.in_projs.0."),
|
||||
(".self_attn.out_proj.", ".self_attn.out_projs.0."),
|
||||
(".ffn.0.", ".linear1."),
|
||||
(".ffn.2.", ".linear2."),
|
||||
(".layer_scale_1.", ".ls1."),
|
||||
(".layer_scale_2.", ".ls2."),
|
||||
(".input_proj.", ".in_proj."),
|
||||
@@ -292,21 +725,39 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
return name.replace(src, dst)
|
||||
return name
|
||||
|
||||
loaded_total = 0
|
||||
loaded_names: set[str] = set()
|
||||
skipped: list[str] = []
|
||||
shape_mismatches: list[tuple[str, str, tuple[int, ...], tuple[int, ...]]] = []
|
||||
for name, tensor in codec_weights:
|
||||
# Try direct name first (e.g. ``quantizer.input_proj.*`` exists
|
||||
# under the same name in both layouts), then the remap (transformer
|
||||
# submodules need ``.linear1.``→``.ff1.`` etc.).
|
||||
tgt = name if name in params_dict else _remap(name)
|
||||
if tgt in params_dict:
|
||||
expected_shape = tuple(params_dict[tgt].shape)
|
||||
actual_shape = tuple(tensor.shape)
|
||||
if expected_shape != actual_shape:
|
||||
shape_mismatches.append((name, tgt, actual_shape, expected_shape))
|
||||
continue
|
||||
default_weight_loader(params_dict[tgt], tensor)
|
||||
loaded_total += 1
|
||||
loaded_names.add(tgt)
|
||||
else:
|
||||
skipped.append(name)
|
||||
|
||||
missing = sorted(set(params_dict) - loaded_names)
|
||||
if missing or skipped or shape_mismatches:
|
||||
raise RuntimeError(
|
||||
"MOSS Audio Tokenizer weights were not fully loaded: "
|
||||
f"loaded={len(loaded_names)}/{len(params_dict)} "
|
||||
f"missing={len(missing)} skipped={len(skipped)} "
|
||||
f"shape_mismatches={len(shape_mismatches)}; "
|
||||
f"first_missing={missing[:5]} "
|
||||
f"first_skipped={skipped[:5]} "
|
||||
f"first_shape_mismatches={shape_mismatches[:3]}"
|
||||
)
|
||||
logger.info(
|
||||
"MOSS Audio Tokenizer weights: loaded=%d/%d skipped=%d (first skipped: %s)",
|
||||
loaded_total,
|
||||
len(loaded_names),
|
||||
len(params_dict),
|
||||
len(skipped),
|
||||
skipped[:3] if skipped else "none",
|
||||
@@ -316,7 +767,15 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
codec.to(device=device, dtype=torch.float32)
|
||||
codec.eval()
|
||||
self._codec = codec
|
||||
self._n_channels = int(getattr(codec_cfg, "number_channels", 1) or 1)
|
||||
inferred_channels = 2 if "v2" in codec_path.lower() else 1
|
||||
self._n_channels = int(
|
||||
getattr(
|
||||
codec_cfg,
|
||||
"number_channels",
|
||||
getattr(codec_cfg, "num_channels", inferred_channels),
|
||||
)
|
||||
or inferred_channels
|
||||
)
|
||||
self._sr_tensor = torch.tensor(int(codec_cfg.sampling_rate), dtype=torch.int32)
|
||||
|
||||
logger.info(
|
||||
@@ -326,7 +785,9 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
self._n_channels,
|
||||
)
|
||||
|
||||
self._maybe_enable_decoder_cudagraph(device)
|
||||
self._configure_decoder_cudagraph(device)
|
||||
if self._codec_streaming and self._streaming_cudagraph_capture_sizes:
|
||||
self._ensure_stream_session()
|
||||
|
||||
# vLLM's track_weights_loading() compares the returned set against
|
||||
# ``self.named_parameters()``. After ``self._codec = codec`` above,
|
||||
@@ -336,28 +797,43 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
|
||||
def _build_codec(self, codec_path: str) -> tuple[Any, nn.Module]:
|
||||
try:
|
||||
from transformers import AutoConfig, AutoModel
|
||||
|
||||
codec_cfg = AutoConfig.from_pretrained(codec_path, trust_remote_code=True)
|
||||
codec = AutoModel.from_config(codec_cfg, trust_remote_code=True)
|
||||
logger.info("Using MOSS Audio Tokenizer remote-code classes from %s", codec_path)
|
||||
codec_cfg = MossAudioTokenizerV2Config.from_pretrained(codec_path)
|
||||
codec = MossAudioTokenizerV2Model(codec_cfg)
|
||||
logger.info("Using vendored MOSS Audio Tokenizer v2 classes from %s", codec_path)
|
||||
return codec_cfg, codec
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to instantiate official MOSS Audio Tokenizer via HF remote code; "
|
||||
"falling back to vendored codec."
|
||||
"Failed to instantiate vendored MOSS Audio Tokenizer v2; falling back to legacy vendored codec."
|
||||
)
|
||||
|
||||
codec_cfg = MossAudioTokenizerConfig.from_pretrained(codec_path)
|
||||
codec = MossAudioTokenizerModel(codec_cfg)
|
||||
return codec_cfg, codec
|
||||
|
||||
def _maybe_enable_decoder_cudagraph(self, device: torch.device) -> None:
|
||||
"""Capture CUDA Graphs for the codec decoder if enforce_eager is False."""
|
||||
def _configure_decoder_cudagraph(self, device: torch.device) -> None:
|
||||
"""Select the codec CUDA Graph path.
|
||||
|
||||
``enforce_eager`` is the single graph on/off switch. If graphing is
|
||||
enabled, ``codec_streaming`` decides whether decode uses the persistent
|
||||
streaming-state wrapper or the offline full-chunk wrapper.
|
||||
"""
|
||||
if getattr(self.vllm_config.model_config, "enforce_eager", True):
|
||||
self._streaming_cudagraph_capture_sizes = []
|
||||
return
|
||||
if self._codec is None:
|
||||
return
|
||||
if self._codec_streaming:
|
||||
logger.info(
|
||||
"MOSS-TTS codec CUDA Graph selected streaming wrapper: capture_sizes=%s",
|
||||
self._streaming_cudagraph_capture_sizes,
|
||||
)
|
||||
return
|
||||
|
||||
self._enable_non_streaming_decoder_cudagraph(device)
|
||||
|
||||
def _enable_non_streaming_decoder_cudagraph(self, device: torch.device) -> None:
|
||||
if self._codec is None:
|
||||
return
|
||||
|
||||
# Read capture sizes from the connector's extra config (same convention
|
||||
# as Qwen3-TTS), falling back to a sensible default covering common
|
||||
|
||||
@@ -126,6 +126,7 @@ def _sample_token(
|
||||
top_k: int,
|
||||
top_p: float,
|
||||
do_sample: bool,
|
||||
generator: torch.Generator | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Top-k + top-p sampling (matches upstream's ``sample_token`` for the
|
||||
inference branch).
|
||||
@@ -152,7 +153,7 @@ def _sample_token(
|
||||
|
||||
probs = F.softmax(logits, dim=-1)
|
||||
flat = probs.reshape(-1, probs.shape[-1])
|
||||
sampled = torch.multinomial(flat, num_samples=1).reshape(probs.shape[:-1])
|
||||
sampled = torch.multinomial(flat, num_samples=1, generator=generator).reshape(probs.shape[:-1])
|
||||
return sampled
|
||||
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ class MossTTSLocalDepthTransformer(nn.Module):
|
||||
text_top_p: float = 1.0,
|
||||
repetition_penalty: float = 1.0,
|
||||
history_per_codebook: list[list[int]] | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Generate one audio frame for batch B.
|
||||
|
||||
@@ -183,7 +184,16 @@ class MossTTSLocalDepthTransformer(nn.Module):
|
||||
local_hidden = hidden[:, 0, :]
|
||||
|
||||
binary_logits = local_text_lm_head(local_hidden).float()
|
||||
binary_choice = _sample_token(binary_logits, text_temperature, text_top_k, text_top_p, do_sample)
|
||||
# This is a binary continue/stop gate. The checkpoint expects sampling
|
||||
# here; greedy argmax is biased toward "continue" and may never stop.
|
||||
binary_choice = _sample_token(
|
||||
binary_logits,
|
||||
text_temperature,
|
||||
text_top_k,
|
||||
text_top_p,
|
||||
do_sample,
|
||||
generator=generator,
|
||||
)
|
||||
should_continue = binary_choice.eq(0)
|
||||
import os as _os
|
||||
|
||||
@@ -209,7 +219,14 @@ class MossTTSLocalDepthTransformer(nn.Module):
|
||||
pos = sel > 0
|
||||
sel = torch.where(pos, sel / repetition_penalty, sel * repetition_penalty)
|
||||
channel_logits.index_copy_(-1, hist_t, sel)
|
||||
channel_token = _sample_token(channel_logits, temperature, top_k, top_p, do_sample)
|
||||
channel_token = _sample_token(
|
||||
channel_logits,
|
||||
temperature,
|
||||
top_k,
|
||||
top_p,
|
||||
do_sample,
|
||||
generator=generator,
|
||||
)
|
||||
codes[:, channel_index] = channel_token
|
||||
|
||||
if channel_index + 1 < n_vq:
|
||||
|
||||
@@ -1139,26 +1139,19 @@ class MossTTSRealtimeTalkerForGeneration(nn.Module):
|
||||
|
||||
info["audio_codes"] = {"current": new_codes, "accumulated": updated_acc}
|
||||
state["step"] = int(state.get("step", 0)) + 1
|
||||
per_req_codes[i] = updated_acc
|
||||
per_req_codes[i] = new_codes.unsqueeze(0)
|
||||
have_codes = True
|
||||
|
||||
if not have_codes:
|
||||
return OmniOutput(text_hidden_states=hidden, multimodal_outputs={})
|
||||
|
||||
# Emit one accumulated-codes tensor per request as a list; the splitter
|
||||
# routes ``element[idx]`` to request idx, so each Stage-1 request decodes
|
||||
# its own codes instead of request 0's.
|
||||
# The realtime variant emits raw codes (no delay pattern), so we signal
|
||||
# that to the chunk processor via a 1-D bool tensor in ``meta.finished``
|
||||
# (already a tensor field on the schema). The processor reads its truth
|
||||
# value as "skip de-delay" only for realtime; the delay path doesn't
|
||||
# populate ``meta`` at all.
|
||||
device = hidden.device
|
||||
# Emit one current-frame tensor per request as a list; the splitter
|
||||
# routes ``element[idx]`` to request idx, so each Stage-1 request receives
|
||||
# only its own newly generated raw codec row.
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden,
|
||||
multimodal_outputs={
|
||||
"codes": {"audio": per_req_codes},
|
||||
"meta": {"finished": torch.tensor([True], dtype=torch.bool, device=device)},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1266,7 +1259,7 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
* Per-step audio generation runs ``MossTTSLocalDepthTransformer``
|
||||
(1-layer GPT2-style local transformer, interleaved RoPE, ``n_vq=12``
|
||||
codebooks -- structurally different from Realtime's Qwen3-style
|
||||
``CodePredictorBaseModel``) inside ``make_omni_output``. Stop
|
||||
``CodePredictorBaseModel``) in the runner-owned ``talker_mtp`` path. Stop
|
||||
condition: the local transformer's binary head selects the
|
||||
"audio_end" candidate (logits index 1).
|
||||
"""
|
||||
@@ -1327,6 +1320,8 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
# Stacked embedding cache built after load_weights() for vectorised
|
||||
# additive-fusion gathers (avoids an n_vq-iteration Python loop).
|
||||
self._stacked_audio_emb_w: torch.Tensor | None = None
|
||||
self.mtp_hidden_size = hidden_size
|
||||
self.talker_mtp_accepts_req_infos = True
|
||||
|
||||
self.gpu_resident_buffer_keys: set[tuple[str, str]] = {
|
||||
("audio_codes", "current"),
|
||||
@@ -1384,6 +1379,15 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
num_rows,
|
||||
):
|
||||
if state is not None and state.get("is_stopping"):
|
||||
forced_im_end_count = int(state.get("_forced_im_end_count", 0))
|
||||
if forced_im_end_count == 1:
|
||||
logger.warning(
|
||||
"MOSS-TTS Local request is still running after "
|
||||
"im_end_token_id=%d was forced once; verify stage-0 "
|
||||
"stop_token_ids/eos includes this id.",
|
||||
self.im_end_token_id,
|
||||
)
|
||||
state["_forced_im_end_count"] = forced_im_end_count + 1
|
||||
logits[row_start:row_end, self.im_end_token_id] = 0.0
|
||||
else:
|
||||
logits[row_start:row_end, self.audio_assistant_slot_token_id] = 0.0
|
||||
@@ -1455,38 +1459,159 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
else torch.full((self.n_vq,), self.audio_pad_token_id, dtype=torch.long, device=device)
|
||||
)
|
||||
|
||||
max_new_frames_req = info_dict.get("max_new_frames")
|
||||
if isinstance(max_new_frames_req, (list, tuple)) and max_new_frames_req:
|
||||
max_new_frames_req = max_new_frames_req[0]
|
||||
try:
|
||||
max_new_frames = int(max_new_frames_req) if max_new_frames_req is not None else -1
|
||||
except (TypeError, ValueError):
|
||||
max_new_frames = -1
|
||||
max_new_frames = info_dict.get("max_new_frames", [-1])[0]
|
||||
|
||||
info_update: dict[str, Any] = {
|
||||
"audio_state": {"is_stopping": False, "step": 0, "max_new_frames": max_new_frames},
|
||||
"audio_codes": {"current": current_codes},
|
||||
"audio_state": {
|
||||
"is_stopping": False,
|
||||
"step": 0,
|
||||
"max_new_frames": max_new_frames,
|
||||
},
|
||||
"audio_codes": {"current": current_codes, "emit": False},
|
||||
"ref_offset": ref_offset + span_len,
|
||||
}
|
||||
return input_ids, embeds, info_update
|
||||
|
||||
# Decode step: input_ids is the forced text token from compute_logits.
|
||||
# Decode step: return the plain forced text-token embedding. The local
|
||||
# depth transformer runs in OmniGPUModelRunner.talker_mtp before the
|
||||
# backbone forward; it adds the freshly sampled audio-code embedding
|
||||
# and stores the generated code frame in model_intermediate_buffer.
|
||||
text_embed = self.model.embed_tokens(input_ids.reshape(1)) # (1, H)
|
||||
prev_codes = (info_dict.get("audio_codes", {}) or {}).get("current")
|
||||
if isinstance(prev_codes, torch.Tensor) and prev_codes.numel() == self.n_vq:
|
||||
codes = prev_codes.to(device=device, dtype=torch.long).unsqueeze(0) # (1, n_vq)
|
||||
combined = text_embed + self._audio_embed(codes)
|
||||
hs = info_dict.get("hidden_states", {}) or {}
|
||||
last_hidden = hs.get("last")
|
||||
if isinstance(last_hidden, torch.Tensor) and last_hidden.numel() > 0:
|
||||
mtp_hidden = last_hidden.to(device=device, dtype=text_embed.dtype).reshape(1, -1)
|
||||
else:
|
||||
combined = text_embed
|
||||
return input_ids, combined, {}
|
||||
mtp_hidden = torch.zeros((1, self.hidden_size), device=device, dtype=text_embed.dtype)
|
||||
return input_ids, text_embed, {"mtp_inputs": (mtp_hidden, torch.zeros_like(mtp_hidden))}
|
||||
|
||||
def postprocess(self, hidden_states: torch.Tensor, **_: Any) -> dict[str, Any]:
|
||||
if hidden_states.numel() == 0:
|
||||
return {}
|
||||
return {"hidden_states": {"last": hidden_states[-1].detach()}}
|
||||
|
||||
@torch.inference_mode()
|
||||
def talker_mtp(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
input_embeds: torch.Tensor,
|
||||
last_talker_hidden: torch.Tensor,
|
||||
text_step: torch.Tensor,
|
||||
do_sample: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
top_k: int | None = None,
|
||||
top_p: float | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
req_infos: list[dict[str, Any]] | None = None,
|
||||
**_: Any,
|
||||
) -> tuple[torch.Tensor, None]:
|
||||
bsz = int(input_embeds.shape[0])
|
||||
dev = input_embeds.device
|
||||
input_embeds_out = input_embeds.reshape(bsz, -1).clone()
|
||||
req_infos = req_infos or [{} for _ in range(bsz)]
|
||||
|
||||
def write_update(info: dict[str, Any], update: dict[str, Any]) -> None:
|
||||
for key, value in update.items():
|
||||
if isinstance(value, dict):
|
||||
target = info.setdefault(key, {})
|
||||
target.update(value)
|
||||
else:
|
||||
info[key] = value
|
||||
|
||||
# Match MOSS-TTS Local v1.5's model-card/SGLang defaults. The generic
|
||||
# stage SamplingParams are for the backbone token loop and should not
|
||||
# silently retune the local depth transformer's audio-channel sampler.
|
||||
audio_temperature = 1.7
|
||||
audio_top_k = 25
|
||||
audio_top_p = 0.8
|
||||
do_sample_val = True if do_sample is None else bool(do_sample)
|
||||
|
||||
for i in range(bsz):
|
||||
info = req_infos[i] if i < len(req_infos) and isinstance(req_infos[i], dict) else {}
|
||||
state = dict(info.get("audio_state", {}) or {})
|
||||
step = int(state.get("step", 0))
|
||||
max_new_frames = int(state.get("max_new_frames", -1))
|
||||
acc = (info.get("audio_codes", {}) or {}).get("accumulated")
|
||||
|
||||
if state.get("is_stopping"):
|
||||
write_update(
|
||||
info,
|
||||
{
|
||||
"audio_state": state,
|
||||
"audio_codes": {
|
||||
"current": input_embeds.new_empty((0, self.n_vq), dtype=torch.long),
|
||||
"emit": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
continue
|
||||
|
||||
rep_window = 50
|
||||
if isinstance(acc, torch.Tensor) and acc.numel() > 0:
|
||||
tail = acc[-rep_window:].long().cpu().tolist()
|
||||
hist_per_cb = [[row[cb] for row in tail] for cb in range(self.n_vq)]
|
||||
else:
|
||||
hist_per_cb = [[] for _ in range(self.n_vq)]
|
||||
|
||||
should_continue_t, new_codes_b = self.local_transformer.generate_frame(
|
||||
last_talker_hidden[i : i + 1],
|
||||
self.audio_lm_heads,
|
||||
self.audio_embeddings,
|
||||
self.local_text_lm_head,
|
||||
n_vq=self.n_vq,
|
||||
do_sample=do_sample_val,
|
||||
temperature=audio_temperature,
|
||||
top_k=audio_top_k,
|
||||
top_p=audio_top_p,
|
||||
repetition_penalty=1.0,
|
||||
history_per_codebook=hist_per_cb,
|
||||
generator=generator,
|
||||
)
|
||||
should_continue = bool(should_continue_t[0].item())
|
||||
new_codes = new_codes_b.squeeze(0).to(device=dev, dtype=torch.long)
|
||||
|
||||
if not should_continue or (0 <= max_new_frames <= step):
|
||||
state["is_stopping"] = True
|
||||
state["step"] = step + 1
|
||||
write_update(
|
||||
info,
|
||||
{
|
||||
"audio_state": state,
|
||||
"audio_codes": {
|
||||
"current": input_embeds.new_empty((0, self.n_vq), dtype=torch.long),
|
||||
"accumulated": acc,
|
||||
"emit": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(acc, torch.Tensor) and acc.numel() > 0:
|
||||
updated_acc = torch.cat([acc.to(dev), new_codes.unsqueeze(0)], dim=0)
|
||||
else:
|
||||
updated_acc = new_codes.unsqueeze(0)
|
||||
|
||||
state["step"] = step + 1
|
||||
current = new_codes.unsqueeze(0)
|
||||
input_embeds_out[i : i + 1] = input_embeds_out[i : i + 1] + self._audio_embed(current).to(
|
||||
dtype=input_embeds_out.dtype
|
||||
)
|
||||
write_update(
|
||||
info,
|
||||
{
|
||||
"audio_state": state,
|
||||
"audio_codes": {
|
||||
"current": current,
|
||||
"accumulated": updated_acc,
|
||||
"emit": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return input_embeds_out, None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Per-step audio generation via local transformer
|
||||
# Package runner-generated audio frames
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def make_omni_output(
|
||||
@@ -1511,91 +1636,41 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
self._batch_state = [(info["audio_state"] if isinstance(info, dict) else {}) for info in info_dicts]
|
||||
self._batch_state_spans = kwargs.get("request_token_spans")
|
||||
|
||||
spans = kwargs.get("request_token_spans")
|
||||
per_req_codes: list[torch.Tensor] = [hidden.new_empty((0, self.n_vq), dtype=torch.long) for _ in info_dicts]
|
||||
per_req_codes: list[torch.Tensor] = []
|
||||
have_codes = False
|
||||
|
||||
if hidden.numel() > 0 and info_dicts:
|
||||
num_rows = hidden.shape[0]
|
||||
if spans is None or len(spans) != len(info_dicts):
|
||||
raise RuntimeError(
|
||||
"MOSS-TTS make_omni_output requires request_token_spans "
|
||||
f"for {len(info_dicts)} request infos, got {0 if spans is None else len(spans)}"
|
||||
)
|
||||
for i, info in enumerate(info_dicts):
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
row_start, row_end = spans[i]
|
||||
row_end = min(int(row_end), num_rows)
|
||||
if row_start >= row_end:
|
||||
continue
|
||||
|
||||
state = info.get("audio_state", {}) or {}
|
||||
if state.get("is_stopping"):
|
||||
continue # already stopped -- no more audio frames
|
||||
|
||||
last_h = hidden[row_end - 1].unsqueeze(0) # (1, H)
|
||||
|
||||
# Sampling parameters mirror Realtime's defaults; repetition
|
||||
# penalty applies over a 50-frame rolling window per codebook.
|
||||
rep_window = 50
|
||||
acc_for_hist = (info.get("audio_codes", {}) or {}).get("accumulated")
|
||||
if isinstance(acc_for_hist, torch.Tensor) and acc_for_hist.numel() > 0:
|
||||
tail = acc_for_hist[-rep_window:].long().cpu().tolist()
|
||||
hist_per_cb = [[row[cb] for row in tail] for cb in range(self.n_vq)]
|
||||
else:
|
||||
hist_per_cb = [[] for _ in range(self.n_vq)]
|
||||
|
||||
should_continue_t, new_codes = self.local_transformer.generate_frame(
|
||||
last_h,
|
||||
self.audio_lm_heads,
|
||||
self.audio_embeddings,
|
||||
self.local_text_lm_head,
|
||||
n_vq=self.n_vq,
|
||||
do_sample=True,
|
||||
temperature=0.8,
|
||||
top_k=30,
|
||||
top_p=0.6,
|
||||
repetition_penalty=1.1,
|
||||
history_per_codebook=hist_per_cb,
|
||||
)
|
||||
should_continue = bool(should_continue_t[0].item())
|
||||
new_codes = new_codes.squeeze(0) # (n_vq,)
|
||||
|
||||
step = int(state.get("step", 0))
|
||||
max_new_frames = int(state.get("max_new_frames", -1))
|
||||
if not should_continue or (0 <= max_new_frames <= step):
|
||||
state["is_stopping"] = True
|
||||
state["step"] = step + 1
|
||||
info["audio_codes"] = {
|
||||
"current": new_codes,
|
||||
"accumulated": (info.get("audio_codes", {}) or {}).get("accumulated"),
|
||||
}
|
||||
continue # don't append the stop frame to accumulated
|
||||
|
||||
acc = (info.get("audio_codes", {}) or {}).get("accumulated")
|
||||
if isinstance(acc, torch.Tensor) and acc.numel() > 0:
|
||||
updated_acc = torch.cat([acc.to(new_codes.device), new_codes.unsqueeze(0)], dim=0)
|
||||
else:
|
||||
updated_acc = new_codes.unsqueeze(0)
|
||||
|
||||
info["audio_codes"] = {"current": new_codes, "accumulated": updated_acc}
|
||||
state["step"] = step + 1
|
||||
per_req_codes[i] = updated_acc
|
||||
for info in info_dicts:
|
||||
current = (info.get("audio_codes", {}) or {}).get("current") if isinstance(info, dict) else None
|
||||
emit = bool((info.get("audio_codes", {}) or {}).get("emit")) if isinstance(info, dict) else False
|
||||
if emit and isinstance(current, torch.Tensor) and current.numel() > 0:
|
||||
codes = current.to(device=hidden.device, dtype=torch.long)
|
||||
if codes.dim() == 1:
|
||||
codes = codes.unsqueeze(0)
|
||||
per_req_codes.append(codes)
|
||||
have_codes = True
|
||||
else:
|
||||
per_req_codes.append(hidden.new_empty((0, self.n_vq), dtype=torch.long))
|
||||
|
||||
if not have_codes:
|
||||
return OmniOutput(text_hidden_states=hidden, multimodal_outputs={})
|
||||
|
||||
# Local-v1.5 emits raw codes (no delay pattern), so signal that to the
|
||||
# chunk processor the same way Realtime does, via ``meta.finished``
|
||||
# (read as "skip de-delay" by ``talker2codec_async_chunk``).
|
||||
device = hidden.device
|
||||
# Local-v1.5 emits the current raw code frame (no delay pattern); the
|
||||
# pipeline routes those rows through ``talker2codec_raw_async_chunk``.
|
||||
per_req_ref_codes: list[torch.Tensor] = []
|
||||
for info in info_dicts:
|
||||
ref_codes = (info.get("codes", {}) or {}).get("ref") if isinstance(info, dict) else None
|
||||
if isinstance(ref_codes, torch.Tensor) and ref_codes.numel() > 0:
|
||||
ref_codes = ref_codes.detach()
|
||||
if ref_codes.dim() == 1 and ref_codes.numel() % self.n_vq == 0:
|
||||
ref_codes = ref_codes.view(-1, self.n_vq)
|
||||
if ref_codes.dim() == 2 and ref_codes.shape[1] == self.n_vq:
|
||||
per_req_ref_codes.append(ref_codes.to(device=hidden.device, dtype=torch.long))
|
||||
continue
|
||||
per_req_ref_codes.append(hidden.new_empty((0, self.n_vq), dtype=torch.long))
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden,
|
||||
multimodal_outputs={
|
||||
"codes": {"audio": per_req_codes},
|
||||
"meta": {"finished": torch.tensor([True], dtype=torch.bool, device=device)},
|
||||
"codes": {"audio": per_req_codes, "ref": per_req_ref_codes},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""CUDA Graph acceleration for the MOSS Audio Tokenizer codec decoder.
|
||||
|
||||
Captures MossAudioTokenizerModel._decode for a set of fixed frame-count
|
||||
Captures MossAudioTokenizerModel._decode_frame for a set of fixed frame-count
|
||||
bucket sizes, then replays the captured graph at inference time to eliminate
|
||||
kernel-launch overhead. Inputs that exceed all captured sizes fall back to
|
||||
eager execution transparently.
|
||||
@@ -24,7 +24,7 @@ logger = init_logger(__name__)
|
||||
|
||||
|
||||
class MossTTSCUDAGraphCodecWrapper:
|
||||
"""CUDA Graph wrapper for MossAudioTokenizerModel._decode.
|
||||
"""CUDA Graph wrapper for MossAudioTokenizerModel._decode_frame.
|
||||
|
||||
Graphs are keyed by padded_T (int). On each call the actual T is
|
||||
bucket-matched to the smallest pre-captured size >= T. The static code
|
||||
@@ -99,7 +99,7 @@ class MossTTSCUDAGraphCodecWrapper:
|
||||
dummy_codes = torch.zeros(nq, 1, size, dtype=torch.long, device=device)
|
||||
dummy_lengths = torch.tensor([size], dtype=torch.long, device=device)
|
||||
with torch.no_grad():
|
||||
_ = self.model._decode(dummy_codes, dummy_lengths)
|
||||
_ = self.model._decode_frame(dummy_codes, dummy_lengths)
|
||||
|
||||
torch.accelerator.synchronize(device)
|
||||
|
||||
@@ -132,13 +132,13 @@ class MossTTSCUDAGraphCodecWrapper:
|
||||
|
||||
# Extra eager warmup inside capture to ensure all kernels are compiled.
|
||||
with torch.no_grad():
|
||||
_ = self.model._decode(static_codes, static_lengths)
|
||||
_ = self.model._decode_frame(static_codes, static_lengths)
|
||||
torch.accelerator.synchronize(device)
|
||||
|
||||
graph = CUDAGraph()
|
||||
with torch.no_grad():
|
||||
with torch.cuda.graph(graph, pool=current_platform.get_global_graph_pool()):
|
||||
static_out = self.model._decode(static_codes, static_lengths)
|
||||
static_out = self.model._decode_frame(static_codes, static_lengths)
|
||||
|
||||
self.graphs[size] = graph
|
||||
self.static_codes[size] = static_codes
|
||||
|
||||
@@ -31,7 +31,7 @@ MOSS_TTS_PIPELINE = PipelineConfig(
|
||||
input_sources=(),
|
||||
owns_tokenizer=True,
|
||||
engine_output_type="latent",
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2codec_async_chunk"),
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2codec_delay_async_chunk"),
|
||||
sampling_constraints={
|
||||
"detokenize": False,
|
||||
},
|
||||
@@ -62,7 +62,7 @@ MOSS_TTS_REALTIME_PIPELINE = PipelineConfig(
|
||||
input_sources=(),
|
||||
owns_tokenizer=True,
|
||||
engine_output_type="latent",
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2codec_async_chunk"),
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2codec_raw_async_chunk"),
|
||||
sampling_constraints={"detokenize": False},
|
||||
),
|
||||
StagePipelineConfig(
|
||||
@@ -91,8 +91,11 @@ MOSS_TTS_LOCAL_PIPELINE = PipelineConfig(
|
||||
input_sources=(),
|
||||
owns_tokenizer=True,
|
||||
engine_output_type="latent",
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2codec_async_chunk"),
|
||||
sampling_constraints={"detokenize": False},
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2codec_raw_async_chunk"),
|
||||
sampling_constraints={
|
||||
"detokenize": False,
|
||||
"stop_token_ids": [151645],
|
||||
},
|
||||
),
|
||||
StagePipelineConfig(
|
||||
stage_id=1,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""OmniVoice pipeline topology (frozen).
|
||||
|
||||
Single-stage diffusion model for text-to-speech.
|
||||
Stage 0: DiT — text → iterative unmasking → codebook tokens → DAC decode → 24kHz audio
|
||||
"""
|
||||
|
||||
from vllm_omni.config.stage_config import (
|
||||
PipelineConfig,
|
||||
StageExecutionType,
|
||||
StagePipelineConfig,
|
||||
)
|
||||
|
||||
OMNIVOICE_PIPELINE = PipelineConfig(
|
||||
model_type="omnivoice",
|
||||
model_arch="OmniVoicePipeline",
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
model_stage="dit",
|
||||
execution_type=StageExecutionType.DIFFUSION,
|
||||
input_sources=(),
|
||||
final_output=True,
|
||||
final_output_type="audio",
|
||||
owns_tokenizer=False,
|
||||
requires_multimodal_data=False,
|
||||
engine_output_type="audio",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -17,7 +17,6 @@ from vllm.logger import init_logger
|
||||
from vllm.model_executor.models.interfaces import SupportsMRoPE, SupportsMultiModal, SupportsPP
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniConditionalGenerationMixin,
|
||||
Qwen2_5OmniThinkerProcessingInfo,
|
||||
)
|
||||
from vllm.model_executor.models.utils import init_vllm_registered_model, maybe_prefix
|
||||
from vllm.model_executor.models.vision import (
|
||||
@@ -39,6 +38,7 @@ from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniThinkerDummyInputsBuilder,
|
||||
Qwen2_5OmniThinkerMultiModalProcessor,
|
||||
Qwen2_5OmniThinkerProcessingInfo,
|
||||
)
|
||||
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights, split_list_into_ranges
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Iterable, Iterator, Mapping
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import PIL.Image as PILImage
|
||||
import torch
|
||||
from torch import nn
|
||||
from transformers.models.qwen2_5_omni.configuration_qwen2_5_omni import (
|
||||
@@ -26,16 +27,21 @@ from vllm.model_executor.models.module_mapping import MultiModelKeys
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniAudioFeatureInputs,
|
||||
Qwen2_5OmniThinkerDummyInputsBuilder,
|
||||
Qwen2_5OmniThinkerProcessingInfo,
|
||||
check_interleaved_audio_video,
|
||||
merge_interleaved_embeddings,
|
||||
)
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniConditionalGenerationMixin as Qwen2_5OmniConditionalGenerationMixinBase,
|
||||
)
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniThinkerMultiModalDataParser as _Qwen2_5OmniThinkerMultiModalDataParserBase,
|
||||
)
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniThinkerMultiModalProcessor as _Qwen2_5OmniThinkerMultiModalProcessorBase,
|
||||
)
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniThinkerProcessingInfo as _Qwen2_5OmniThinkerProcessingInfoBase,
|
||||
)
|
||||
from vllm.model_executor.models.qwen2_5_vl import (
|
||||
Qwen2_5_VisionTransformer,
|
||||
Qwen2_5_VLImageEmbeddingInputs,
|
||||
@@ -56,12 +62,13 @@ from vllm.multimodal.inputs import (
|
||||
MultiModalFeatureSpec,
|
||||
MultiModalKwargsItems,
|
||||
)
|
||||
from vllm.multimodal.parse import MultiModalDataItems
|
||||
from vllm.multimodal.parse import MultiModalDataItems, VideoProcessorItems
|
||||
from vllm.multimodal.processing.processor import (
|
||||
MultiModalPromptUpdates,
|
||||
PlaceholderFeaturesInfo,
|
||||
)
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.utils.collection_utils import is_list_of
|
||||
|
||||
from vllm_omni.quantization.component_config import (
|
||||
resolve_encoder_quant_config,
|
||||
@@ -74,11 +81,143 @@ except (ImportError, ModuleNotFoundError):
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _presampled_videos_hf_kwargs(
|
||||
mm_data: Mapping[str, object],
|
||||
mm_kwargs: Mapping[str, object],
|
||||
) -> Mapping[str, object]:
|
||||
"""Adjust HF video kwargs for videos pre-sampled by vLLM's video loader.
|
||||
|
||||
When ``video_metadata`` is present (emitted by
|
||||
``Qwen2_5OmniVideoProcessorItems``), the frames were already sampled
|
||||
according to ``media_io_kwargs``, so the HF processor must not re-sample
|
||||
them, and it needs the sampled fps (instead of its default) to compute
|
||||
``video_second_per_grid`` correctly. Otherwise the audio/video temporal
|
||||
alignment breaks under ``use_audio_in_video=True``.
|
||||
"""
|
||||
video_metadata = mm_data.get("video_metadata")
|
||||
if not video_metadata:
|
||||
return mm_kwargs
|
||||
|
||||
mm_kwargs = dict(mm_kwargs)
|
||||
videos_kwargs = dict(mm_kwargs.get("videos_kwargs") or {})
|
||||
videos_kwargs["do_sample_frames"] = False
|
||||
|
||||
def _compute_sampled_video_fps(metadata) -> float | None:
|
||||
duration = getattr(metadata, "duration", None)
|
||||
indices = getattr(metadata, "frames_indices", None)
|
||||
if not duration or not indices or duration <= 0:
|
||||
return None
|
||||
return len(indices) / float(duration)
|
||||
|
||||
# An explicit user-provided fps takes precedence.
|
||||
if "fps" not in videos_kwargs:
|
||||
fps_values = [_compute_sampled_video_fps(m) for m in video_metadata]
|
||||
# HF accepts a single fps per call and uses it for every video's
|
||||
# video_second_per_grid.
|
||||
known_fps = [fps for fps in fps_values if fps is not None]
|
||||
unique_fps = set(known_fps)
|
||||
if len(unique_fps) == 1:
|
||||
videos_kwargs["fps"] = known_fps[0]
|
||||
elif len(unique_fps) > 1:
|
||||
logger.warning(
|
||||
f"Mixed sampled FPS {sorted(unique_fps)} in one request; HF accepts a single fps, using {known_fps[0]}."
|
||||
)
|
||||
videos_kwargs["fps"] = known_fps[0]
|
||||
|
||||
mm_kwargs["videos_kwargs"] = videos_kwargs
|
||||
return mm_kwargs
|
||||
|
||||
|
||||
class Qwen2_5OmniVideoProcessorItems(VideoProcessorItems):
|
||||
"""Video items that carry the loader's ``(frames, metadata)`` tuples.
|
||||
|
||||
The tuples are kept in ``data`` so that mm hashing and cache-miss
|
||||
re-parsing retain the metadata; ``get_processor_data`` unpacks them into
|
||||
the ``videos`` + ``video_metadata`` arguments of the HF processor.
|
||||
"""
|
||||
|
||||
def get_processor_data(self) -> Mapping[str, object]:
|
||||
from transformers.video_utils import VideoMetadata
|
||||
|
||||
videos = [frames for frames, _ in self.data]
|
||||
video_metadata = [
|
||||
VideoMetadata(**{k: v for k, v in metadata.items() if k != "do_sample_frames"}) for _, metadata in self.data
|
||||
]
|
||||
return {"videos": videos, "video_metadata": video_metadata}
|
||||
|
||||
|
||||
class Qwen2_5OmniThinkerMultiModalDataParser(_Qwen2_5OmniThinkerMultiModalDataParserBase):
|
||||
def _parse_video_data(self, data):
|
||||
if data is None or isinstance(data, dict) or self.is_embeddings(data):
|
||||
return super()._parse_video_data(data)
|
||||
|
||||
# Normalize to a list of per-video items (same as the base parser).
|
||||
if (is_list_of(data, PILImage.Image) and len(data) > 0) or (
|
||||
isinstance(data, (np.ndarray, torch.Tensor)) and data.ndim == 4
|
||||
):
|
||||
data_items = [data]
|
||||
elif isinstance(data, (np.ndarray, torch.Tensor)):
|
||||
data_items = [elem for elem in data]
|
||||
elif isinstance(data, tuple) and len(data) == 2:
|
||||
data_items = [data]
|
||||
else:
|
||||
data_items = data
|
||||
|
||||
videos_with_metadata = [self._get_video_with_metadata(item) for item in data_items]
|
||||
|
||||
# Metadata is optional: videos fetched by vLLM's media connector
|
||||
# arrive as (frames, metadata) tuples, while plain arrays (e.g.
|
||||
# offline `multi_modal_data` or dummy profiling inputs) carry no
|
||||
# metadata and parse as before.
|
||||
if any(metadata is None for _, metadata in videos_with_metadata):
|
||||
return super()._parse_video_data(data)
|
||||
|
||||
return Qwen2_5OmniVideoProcessorItems(
|
||||
videos_with_metadata,
|
||||
metadata=[metadata for _, metadata in videos_with_metadata],
|
||||
)
|
||||
|
||||
|
||||
class Qwen2_5OmniThinkerProcessingInfo(_Qwen2_5OmniThinkerProcessingInfoBase):
|
||||
def get_data_parser(self):
|
||||
feature_extractor = self.get_feature_extractor()
|
||||
|
||||
return Qwen2_5OmniThinkerMultiModalDataParser(
|
||||
spatial_merge_size=self.get_hf_config().vision_config.spatial_merge_size,
|
||||
target_sr=feature_extractor.sampling_rate,
|
||||
target_channels=self.get_target_channels(),
|
||||
expected_hidden_size=self._get_expected_hidden_size(),
|
||||
)
|
||||
|
||||
|
||||
class Qwen2_5OmniThinkerMultiModalProcessor(
|
||||
_Qwen2_5OmniThinkerMultiModalProcessorBase,
|
||||
):
|
||||
"""Override to fix use_audio_in_video detection when mm cache returns None."""
|
||||
|
||||
def _cached_apply_hf_processor(self, inputs, timing_ctx):
|
||||
# If use_audio_in_video, process video and audio together; otherwise,
|
||||
# skip cache to avoid errors.
|
||||
# Prevents partial cache hits from causing processing failures.
|
||||
if inputs.hf_processor_mm_kwargs.get("use_audio_in_video"):
|
||||
return self._apply_hf_processor(inputs, timing_ctx)
|
||||
return super()._cached_apply_hf_processor(inputs, timing_ctx)
|
||||
|
||||
def _call_hf_processor(
|
||||
self,
|
||||
prompt: str,
|
||||
mm_data: Mapping[str, object],
|
||||
mm_kwargs: Mapping[str, object],
|
||||
tok_kwargs: Mapping[str, object],
|
||||
):
|
||||
mm_kwargs = _presampled_videos_hf_kwargs(mm_data, mm_kwargs)
|
||||
return super()._call_hf_processor(
|
||||
prompt=prompt,
|
||||
mm_data=mm_data,
|
||||
mm_kwargs=mm_kwargs,
|
||||
tok_kwargs=tok_kwargs,
|
||||
)
|
||||
|
||||
def _maybe_apply_prompt_updates(
|
||||
self,
|
||||
mm_items: MultiModalDataItems,
|
||||
|
||||
@@ -335,19 +335,10 @@ class Qwen2Model(nn.Module):
|
||||
("gate_up_proj", "up_proj", 1),
|
||||
]
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
get_cache_scale = getattr(self.quant_config, "get_cache_scale", None)
|
||||
loaded_params: set[str] = set()
|
||||
for name, loaded_weight in weights:
|
||||
if "rotary_emb.inv_freq" in name:
|
||||
continue
|
||||
if get_cache_scale is not None and (scale_name := get_cache_scale(name)):
|
||||
# Loading kv cache quantization scales
|
||||
param = params_dict[scale_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
loaded_weight = loaded_weight if loaded_weight.dim() == 0 else loaded_weight[0]
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(scale_name)
|
||||
continue
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
continue
|
||||
|
||||
@@ -9,6 +9,7 @@ Stage 2: Code2Wav — RVQ codes → audio waveform
|
||||
|
||||
from transformers import Qwen3OmniMoeConfig
|
||||
|
||||
from vllm_omni.config.endpoint_policy import EndpointRestriction, OmniServingCapability
|
||||
from vllm_omni.config.stage_config import (
|
||||
PipelineConfig,
|
||||
StageExecutionType,
|
||||
@@ -21,6 +22,12 @@ _PROC = "vllm_omni.model_executor.stage_input_processors.qwen3_omni"
|
||||
QWEN3_OMNI_PIPELINE = PipelineConfig(
|
||||
model_type="qwen3_omni_moe",
|
||||
model_arch="Qwen3OmniMoeForConditionalGeneration",
|
||||
endpoint_restrictions=(
|
||||
EndpointRestriction(
|
||||
OmniServingCapability.COMPLETIONS,
|
||||
"Qwen3-Omni requires chat template structure for thinker-talker handoff. Use /v1/chat/completions instead.",
|
||||
),
|
||||
),
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
@@ -44,7 +51,6 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
|
||||
input_sources=(0,),
|
||||
hf_config_name="talker_config",
|
||||
engine_output_type="latent",
|
||||
custom_process_input_func=f"{_PROC}.thinker2talker",
|
||||
sync_process_input_func=f"{_PROC}.thinker2talker_token_only",
|
||||
custom_process_next_stage_input_func=(f"{_PROC}.talker2code2wav_full_payload"),
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2code2wav_async_chunk"),
|
||||
@@ -62,7 +68,6 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
|
||||
final_output_type="audio",
|
||||
hf_config_name="thinker_config",
|
||||
engine_output_type="audio",
|
||||
custom_process_input_func=f"{_PROC}.talker2code2wav",
|
||||
sampling_constraints={"detokenize": True},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -74,7 +74,6 @@ from vllm.model_executor.models.module_mapping import MultiModelKeys
|
||||
from vllm.model_executor.models.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniAudioFeatureInputs,
|
||||
Qwen2_5OmniThinkerDummyInputsBuilder,
|
||||
Qwen2_5OmniThinkerMultiModalProcessor,
|
||||
check_interleaved_audio_video,
|
||||
merge_interleaved_embeddings,
|
||||
)
|
||||
@@ -120,6 +119,9 @@ from vllm.transformers_utils.processor import cached_processor_from_config
|
||||
|
||||
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker import (
|
||||
Qwen2_5OmniConditionalGenerationMixin,
|
||||
Qwen2_5OmniThinkerMultiModalDataParser,
|
||||
Qwen2_5OmniThinkerMultiModalProcessor,
|
||||
_presampled_videos_hf_kwargs,
|
||||
)
|
||||
from vllm_omni.quantization.component_config import (
|
||||
PRE_QUANTIZED_METHODS,
|
||||
@@ -621,6 +623,19 @@ class Qwen3OmniMoeThinkerProcessingInfo(Qwen2AudioProcessingInfo, Qwen2_5_VLProc
|
||||
assert isinstance(feature_extractor, WhisperFeatureExtractor)
|
||||
return feature_extractor
|
||||
|
||||
def get_data_parser(self):
|
||||
feature_extractor = self.get_feature_extractor()
|
||||
|
||||
# Install the Omni parser, which keeps video metadata from vLLM's
|
||||
# video loader (see Qwen2_5OmniVideoProcessorItems); the parser
|
||||
# inherited from Qwen2AudioProcessingInfo would drop it.
|
||||
return Qwen2_5OmniThinkerMultiModalDataParser(
|
||||
spatial_merge_size=self.get_hf_config().vision_config.spatial_merge_size,
|
||||
target_sr=feature_extractor.sampling_rate,
|
||||
target_channels=1,
|
||||
expected_hidden_size=self._get_expected_hidden_size(),
|
||||
)
|
||||
|
||||
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
||||
return {"audio": None, "image": None, "video": None}
|
||||
|
||||
@@ -671,8 +686,11 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
|
||||
tok_kwargs: Mapping[str, object],
|
||||
) -> BatchFeature:
|
||||
mm_data = dict(mm_data)
|
||||
mm_kwargs = dict(mm_kwargs)
|
||||
audios = mm_data.pop("audios", [])
|
||||
|
||||
mm_kwargs = dict(_presampled_videos_hf_kwargs(mm_data, mm_kwargs))
|
||||
|
||||
def pad_to_hop_length(x: np.ndarray, hop_length: int) -> np.ndarray:
|
||||
length = x.shape[-1]
|
||||
if length % hop_length != 0:
|
||||
@@ -927,9 +945,13 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
|
||||
|
||||
audio_in_video_item_idx += 1
|
||||
|
||||
second_per_grid_ts = hf_processor_mm_kwargs.get("second_per_grid_ts", None)
|
||||
if second_per_grid_ts:
|
||||
video_second_per_grid_t = second_per_grid_ts[item_idx]
|
||||
# Prefer the HF-computed value (temporal_patch_size / sampled fps)
|
||||
# over request kwargs;
|
||||
second_per_grid_ts = out_mm_data.get("second_per_grid_ts")
|
||||
if second_per_grid_ts is None:
|
||||
second_per_grid_ts = hf_processor_mm_kwargs.get("second_per_grid_ts", None)
|
||||
if second_per_grid_ts is not None:
|
||||
video_second_per_grid_t = float(second_per_grid_ts[item_idx])
|
||||
else:
|
||||
video_second_per_grid_t = 2.0
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from vllm_omni.model_executor.models.common.qwen3_code_predictor import (
|
||||
CodePredictorWrapper,
|
||||
CodePredictorWrapperConfig,
|
||||
)
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
from .configuration_qwen3_tts import Qwen3TTSTalkerCodePredictorConfig, Qwen3TTSTalkerConfig
|
||||
|
||||
@@ -21,7 +22,7 @@ Qwen3TTSTalkerCodePredictorModelVLLM = CodePredictorBaseModel
|
||||
|
||||
|
||||
class Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM(CodePredictorWrapper):
|
||||
"""Qwen3-TTS code predictor (CUDA graphs, per-call sampling, projection)."""
|
||||
"""Qwen3-TTS code predictor (per-call sampling, projection)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -35,7 +36,7 @@ class Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM(CodePredictorWrapp
|
||||
vllm_config=vllm_config,
|
||||
cp_config=config,
|
||||
wrapper_config=CodePredictorWrapperConfig(
|
||||
use_cuda_graphs=True,
|
||||
use_cuda_graphs=current_omni_platform.is_npu(),
|
||||
use_parallel_embedding=False,
|
||||
use_projection=(config.hidden_size != talker_config.hidden_size),
|
||||
return_proj_buf=False,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from collections.abc import Iterable, Mapping
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
@@ -314,6 +314,12 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
|
||||
# tensor read; postprocess receives the tail-only slice instead, which
|
||||
# avoids ~18 ms merge + ~6 ms write per step (Sy0307 profile, #3665).
|
||||
self.requires_full_prefix_cached_hidden_states = False
|
||||
# Stage 1 (code2wav) consumes runtime audio codes, not hidden states,
|
||||
# so the inter-stage pooler payload never needs a CPU hidden-states
|
||||
# view. Opting out lets the runner skip the per-step blocking
|
||||
# ``hidden_states[:n].to("cpu")`` (measured at 24% of stage-0 on-CPU
|
||||
# samples at c=64 on H20).
|
||||
self.omni_pooler_payload_include_hidden = False
|
||||
# ``codes.audio`` is only needed for future prefix-hit reconstruction
|
||||
# after a request has produced codec rows. Keep per-step rows on GPU and
|
||||
# materialize the CPU OmniTensorPrefixCache entry once at completion.
|
||||
@@ -323,7 +329,33 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
|
||||
# OmniGPUModelRunner will store talker_mtp output under this key in
|
||||
# per-request additional_information.
|
||||
self.talker_mtp_output_key = ("codes", "audio")
|
||||
|
||||
self.talker_mtp_graph_safe = True
|
||||
# talker_mtp samples with per-row generators, so explicitly-seeded
|
||||
# requests stay batched instead of one scalar forward per row (#4883).
|
||||
# This is only valid while talker_mtp receives the *unpadded* active
|
||||
# batch, i.e. when it is NOT graph-wrapped. The runner wraps talker_mtp
|
||||
# (padded batches + a single captured RNG stream) whenever full
|
||||
# cudagraphs are enabled, so disable per-row generators in that case.
|
||||
# Consequence: under full cudagraphs, per-request ``tts_local_seed`` is
|
||||
# not reproducible (matching Qwen3-Omni, which has no per-row seeding);
|
||||
# eager mode keeps the per-row generator fast-path.
|
||||
# TODO(#4923): the model should not read ``cudagraph_mode`` — it is an
|
||||
# upstream (runner) concern, and the runner already re-derives the same
|
||||
# wrap decision in ``_init_talker_mtp``. Once all TTS models declare
|
||||
# ``talker_mtp_graph_safe`` we should move this gating into the model
|
||||
# runner (models only expose the static capability flag, and users can
|
||||
# then configure ``cudagraph_mode`` freely). This also means mtp seeding
|
||||
# is silently dropped when the decode batch > 1 under full cudagraphs;
|
||||
# the follow-up should refactor the per-row generator in the runner to
|
||||
# be cudagraph-safe so seeded batches stay reproducible.
|
||||
cudagraph_mode = getattr(vllm_config.compilation_config, "cudagraph_mode", None)
|
||||
talker_mtp_graph_wrapped = (
|
||||
self.talker_mtp_graph_safe and cudagraph_mode is not None and cudagraph_mode.has_full_cudagraphs()
|
||||
)
|
||||
self.talker_mtp_accepts_per_row_generators = not talker_mtp_graph_wrapped
|
||||
self.use_async_omni_output = True
|
||||
self.eager_omni_postprocess_before_async_output = True
|
||||
self.omni_pooler_payload_include_hidden = False
|
||||
self.model = Qwen3Model(vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model"))
|
||||
|
||||
if get_pp_group().is_last_rank:
|
||||
@@ -1060,6 +1092,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
|
||||
top_k: int | None = None,
|
||||
top_p: float | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
generators: Sequence[torch.Generator | None] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""GPU fast-path used by OmniGPUModelRunner to predict residual codebooks (1..Q-1).
|
||||
@@ -1099,6 +1132,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
|
||||
top_k=top_k,
|
||||
top_p=top_p,
|
||||
generator=generator,
|
||||
generators=generators,
|
||||
) # [B, Q]
|
||||
|
||||
# Map invalid layer-0 ids (e.g. EOS) to PAD=0 so SpeechTokenizer sees only real codes.
|
||||
|
||||
@@ -26,6 +26,7 @@ VOXCPM2_PIPELINE = PipelineConfig(
|
||||
final_output_type="audio",
|
||||
owns_tokenizer=True,
|
||||
engine_output_type="audio",
|
||||
scheduler_cls="vllm_omni.model_executor.models.voxcpm2.scheduler.VoxCPM2OmniARAsyncScheduler",
|
||||
sampling_constraints={
|
||||
"detokenize": False,
|
||||
"stop_token_ids": [1],
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _VoxCPM2RuntimeConfig:
|
||||
enable_profiling: bool = False
|
||||
enable_nvtx_profile: bool = False
|
||||
enable_loc_dit_layer_nvtx: bool = False
|
||||
enable_loc_dit_fused_qkv: bool = True
|
||||
enable_loc_dit_fused_mlp: bool = True
|
||||
enable_loc_dit_zero_dt_cache: bool = True
|
||||
enable_loc_dit_fast_rope: bool = False
|
||||
enable_loc_dit_skip_qkv_contig: bool = True
|
||||
enable_loc_dit_reduce_overhead_no_cg: bool = False
|
||||
enable_loc_dit_fullgraph_no_cg: bool = False
|
||||
cfg_cutoff_ratio: float = 1.0
|
||||
decode_graph_capture_policy: str = "all"
|
||||
enable_vae_cuda_graph: bool = False
|
||||
enable_cfm_cuda_graph: bool = False
|
||||
enable_cfm_prealloc_output: bool = False
|
||||
enable_batched_cfm: bool = True
|
||||
deterministic_cfm_noise: bool = False
|
||||
deterministic_cfm_seed: int = 20260601
|
||||
audio_emit_every: int = 1
|
||||
vae_decode_every: int = 3
|
||||
enable_delayed_audio_copy: bool = False
|
||||
delayed_audio_copy_use_events: bool = False
|
||||
coalesce_audio_d2h: bool = True
|
||||
enable_batched_vae_decode: bool = True
|
||||
enable_batched_fsq_fusion: bool = False
|
||||
batched_fsq_fusion_max_batch: int = 32
|
||||
enable_batched_prefill_tail: bool = False
|
||||
enable_unified_decode_graph: bool = True
|
||||
unified_decode_graph_max_batch_size: int = 64
|
||||
|
||||
@classmethod
|
||||
def from_vllm_config(cls, vllm_config: Any) -> _VoxCPM2RuntimeConfig:
|
||||
model_config = vllm_config.model_config
|
||||
raw = getattr(model_config, "voxcpm2_runtime_config", None)
|
||||
if raw is None:
|
||||
raw = getattr(getattr(model_config, "hf_config", None), "voxcpm2_runtime_config", None)
|
||||
if raw is None:
|
||||
return cls()
|
||||
if hasattr(raw, "to_dict"):
|
||||
raw = raw.to_dict()
|
||||
elif not isinstance(raw, dict) and hasattr(raw, "__dict__"):
|
||||
raw = vars(raw)
|
||||
if not isinstance(raw, dict):
|
||||
logger.warning("Ignoring invalid voxcpm2_runtime_config=%r; expected a dict.", raw)
|
||||
return cls()
|
||||
|
||||
fields = {field.name: field for field in dataclasses.fields(cls)}
|
||||
values: dict[str, Any] = {}
|
||||
for key, value in raw.items():
|
||||
if key not in fields:
|
||||
logger.warning("Ignoring unknown VoxCPM2 runtime config key: %s", key)
|
||||
continue
|
||||
default = getattr(cls(), key)
|
||||
values[key] = cls._coerce_value(key, value, default)
|
||||
return cls(**values)._normalized()
|
||||
|
||||
def _normalized(self) -> _VoxCPM2RuntimeConfig:
|
||||
cfg = self
|
||||
if cfg.enable_batched_cfm and cfg.enable_cfm_cuda_graph:
|
||||
logger.warning(
|
||||
"VoxCPM2 batched CFM and CFM CUDA Graph are mutually exclusive; "
|
||||
"disabling CFM CUDA Graph and keeping batched CFM."
|
||||
)
|
||||
cfg = dataclasses.replace(cfg, enable_cfm_cuda_graph=False)
|
||||
if cfg.enable_cfm_prealloc_output and not cfg.enable_cfm_cuda_graph:
|
||||
logger.warning("VoxCPM2 CFM preallocated output requires CFM CUDA Graph; disabling it.")
|
||||
cfg = dataclasses.replace(cfg, enable_cfm_prealloc_output=False)
|
||||
if cfg.delayed_audio_copy_use_events and not cfg.enable_delayed_audio_copy:
|
||||
logger.warning("VoxCPM2 delayed audio copy events require delayed audio copy; disabling event polling.")
|
||||
cfg = dataclasses.replace(cfg, delayed_audio_copy_use_events=False)
|
||||
if cfg.enable_batched_vae_decode and (
|
||||
cfg.enable_delayed_audio_copy or cfg.audio_emit_every > 1 or cfg.enable_vae_cuda_graph
|
||||
):
|
||||
logger.warning(
|
||||
"VoxCPM2 batched VAE decode is incompatible with delayed audio copy, "
|
||||
"audio_emit_every > 1, and VAE CUDA Graph; disabling batched VAE decode."
|
||||
)
|
||||
cfg = dataclasses.replace(cfg, enable_batched_vae_decode=False)
|
||||
if cfg.enable_unified_decode_graph:
|
||||
if cfg.enable_cfm_cuda_graph:
|
||||
logger.info("VoxCPM2 unified decode graph supersedes independent CFM CUDA Graph; disabling it.")
|
||||
cfg = dataclasses.replace(cfg, enable_cfm_cuda_graph=False, enable_cfm_prealloc_output=False)
|
||||
if not cfg.enable_batched_cfm:
|
||||
logger.info("VoxCPM2 unified decode graph requires batched CFM; enabling it.")
|
||||
cfg = dataclasses.replace(cfg, enable_batched_cfm=True)
|
||||
return cfg
|
||||
|
||||
def unified_decode_graph_available(self, *, use_cuda_graph: bool) -> bool:
|
||||
return bool(use_cuda_graph and self.enable_unified_decode_graph and not self.deterministic_cfm_noise)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_value(key: str, value: Any, default: Any) -> Any:
|
||||
if isinstance(default, bool):
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("1", "true", "yes", "on")
|
||||
return bool(value)
|
||||
if isinstance(default, int) and not isinstance(default, bool):
|
||||
value = int(value)
|
||||
if key in {"audio_emit_every", "vae_decode_every", "batched_fsq_fusion_max_batch"}:
|
||||
return max(1, value)
|
||||
return value
|
||||
if isinstance(default, float):
|
||||
if key == "cfg_cutoff_ratio":
|
||||
return min(1.0, max(0.0, float(value)))
|
||||
return float(value)
|
||||
if isinstance(default, str):
|
||||
return str(value)
|
||||
return value
|
||||
@@ -0,0 +1,42 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
from vllm_omni.core.sched.omni_ar_scheduler import OmniARAsyncScheduler
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
from .runtime_config import _VoxCPM2RuntimeConfig
|
||||
|
||||
|
||||
class VoxCPM2OmniARAsyncScheduler(OmniARAsyncScheduler):
|
||||
"""VoxCPM2 scheduler variant for full unified decode graph serving.
|
||||
|
||||
VoxCPM2's full unified decode graph only applies to pure decode batches.
|
||||
When a decode-ready request is already running, this scheduler defers new
|
||||
waiting admissions for the current tick so the decode batch can stay on the
|
||||
VoxCPM2 graph path. This is a model-local serving policy, not a generic AR
|
||||
scheduler rule.
|
||||
"""
|
||||
|
||||
def _unified_decode_graph_enabled(self) -> bool:
|
||||
runtime_config = _VoxCPM2RuntimeConfig.from_vllm_config(self.vllm_config)
|
||||
return runtime_config.unified_decode_graph_available(use_cuda_graph=current_omni_platform.is_cuda())
|
||||
|
||||
def _should_defer_waiting_for_unified_decode_graph(self) -> bool:
|
||||
if not self._unified_decode_graph_enabled():
|
||||
return False
|
||||
if not self.waiting or not self.running:
|
||||
return False
|
||||
|
||||
for request in self.running:
|
||||
if getattr(request, "status", None) != RequestStatus.RUNNING or request.is_finished():
|
||||
continue
|
||||
if self._get_confirmed_num_computed_tokens(request) >= request.num_prompt_tokens:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _should_defer_waiting_admission(self) -> bool:
|
||||
return self._should_defer_waiting_for_unified_decode_graph()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -209,7 +209,12 @@ class VoxtralTTSForConditionalGeneration(
|
||||
|
||||
def tts_preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict | None):
|
||||
self.post_process_idx = 0
|
||||
audio_tokens = info_dict.pop("audio", None)
|
||||
codes = info_dict.get("codes")
|
||||
audio_tokens = codes.get("audio") if isinstance(codes, Mapping) else None
|
||||
if audio_tokens is None:
|
||||
# Backward compatible with request state written before #4527 moved
|
||||
# Voxtral TTS feedback frames under ``codes.audio``.
|
||||
audio_tokens = info_dict.pop("audio", None)
|
||||
if audio_tokens is not None:
|
||||
kwargs = {"audio_tokens": audio_tokens.to(input_ids.device)}
|
||||
multimodal_embeddings = self.model.embed_multimodal(**kwargs)
|
||||
|
||||
Reference in new issue
Block a user