Merge main into dev/vllm-align for vLLM 0.31 release

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
This commit is contained in:
tzhouam committed 2026-10-03 21:05:17 +08:00
commit 008fa25263
252 files changed
+27589 -1237

No files matched your search

@@ -16,6 +16,14 @@ from vllm.platforms import current_platform
from vllm_omni.platforms import current_omni_platform
def _depth_layer_compile_options() -> dict[str, bool]:
"""Keep ROCm away from the unstable Triton GEMM autotune path."""
return {
"epilogue_fusion": False,
"max_autotune": not current_platform.is_rocm(),
}
def sample_logits(
logits: torch.Tensor,
temperature: float,
@@ -283,7 +291,7 @@ class BreezeDepthDecoder(nn.Module):
BreezeDepthLayer.forward,
fullgraph=True,
dynamic=True,
options={"epilogue_fusion": False, "max_autotune": True},
options=_depth_layer_compile_options(),
)
self._compiled_sampler = torch.compile(sample_graph_logits, fullgraph=True, dynamic=False)
entry = BreezeDepthGraph(
@@ -0,0 +1,97 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Fused Gumbel selection after native nucleus probability reductions.
Keep ATen's top-k threshold, sort, softmax and cumulative sum, including
boundary ties. Random numbers remain indexed by original token id. Only the
final mask, Gumbel transform and argmax are fused; no vocabulary scatter is
needed. This avoids changing which tokens survive a nucleus boundary through
different reduction orders.
"""
import torch
from vllm.triton_utils import tl, triton
@triton.jit
def _sorted_nucleus_gumbel(
logits,
indices,
probabilities,
cumulative,
uniforms,
output,
vocab: tl.constexpr,
logits_stride: tl.constexpr,
token_stride: tl.constexpr,
uniform_stride: tl.constexpr,
top_p: tl.constexpr,
block: tl.constexpr,
):
row = tl.program_id(0)
offsets = tl.arange(0, block)
valid = offsets < vocab
values = tl.load(logits + row * logits_stride + offsets * token_stride, valid, other=-float("inf")).to(tl.float32)
if top_p < 1.0:
ids = tl.load(indices + row * vocab + offsets, valid, other=0)
probs = tl.load(probabilities + row * vocab + offsets, valid, other=0)
cdf = tl.load(cumulative + row * vocab + offsets, valid, other=0)
values = tl.where((cdf - probs) >= top_p, -float("inf"), values)
else:
ids = offsets
noise = tl.load(uniforms + row * uniform_stride + ids, valid, other=0.5)
score = values - tl.extra.cuda.libdevice.log(-tl.extra.cuda.libdevice.log(noise))
# torch.argmax returns the first original token for a tie or NaN.
first_nan = tl.min(tl.where(valid & (score != score), ids, 2147483647), 0)
maximum = tl.max(tl.where(valid, score, -float("inf")), 0)
winner = tl.min(tl.where(valid & (score == maximum), ids, 2147483647), 0)
tl.store(output + row, tl.where(first_nan < 2147483647, first_nan, winner))
def sample_top_k_top_p_gumbel(
logits: torch.Tensor, uniforms: torch.Tensor, *, top_k: int, top_p: float
) -> torch.Tensor:
"""Sample CUDA rows without scattering sorted logits back to the vocabulary.
Uniforms are caller-owned FP32 values with the same shape as logits; their
generation and generator advancement are deliberately outside this kernel.
Softmax/scan use the reference ATen operations. The Gumbel logarithms
still use libdevice, so this is not a universal bitwise-equivalence claim.
"""
if logits.ndim != 2 or not logits.is_cuda or uniforms.device != logits.device or torch.version.hip is not None:
raise ValueError("fused sampling requires two-dimensional CUDA logits and uniforms")
if uniforms.shape != logits.shape or uniforms.dtype != torch.float32 or uniforms.stride(1) != 1:
raise ValueError("uniforms must be FP32 with the logits shape and contiguous rows")
rows, vocab = logits.shape
# The stored-mode caller disables top-k for both 0 and negative values
# (vLLM commonly uses -1). Keep that behavior when fusion is enabled.
top_k = max(top_k, 0)
if vocab == 0 or top_k > vocab or not 0 < top_p <= 1:
raise ValueError("invalid top-k/top-p sampling parameters")
if top_k:
threshold = logits.topk(top_k, dim=-1).values[:, -1:]
logits = logits.masked_fill(logits < threshold, -float("inf"))
indices = probabilities = cumulative = None
if top_p < 1:
logits, indices = logits.sort(dim=-1, descending=True)
probabilities = logits.softmax(dim=-1, dtype=torch.float32)
cumulative = probabilities.cumsum(-1)
result = torch.empty((rows, 1), dtype=torch.long, device=logits.device)
if rows:
_sorted_nucleus_gumbel[(rows,)](
logits,
indices,
probabilities,
cumulative,
uniforms,
result,
vocab,
logits.stride(0),
logits.stride(1),
uniforms.stride(0),
top_p,
triton.next_power_of_2(vocab),
num_warps=4,
enable_fp_fusion=False,
)
return result
@@ -219,7 +219,8 @@ class ISTFTHead(FourierHead):
# phase = torch.atan2(y, x)
# S = mag * torch.exp(phase * 1j)
# better directly produce the complex value
S = mag * (x + 1j * y)
# float32 operands: bf16 promotes to bcomplex32, which torch.fft cannot handle
S = mag.float() * (x.float() + 1j * y.float())
audio, audio_buffer, window_buffer = self.istft(
S, audio_buffer=audio_buffer, window_buffer=window_buffer, streaming=streaming, last_chunk=last_chunk
)
@@ -18,6 +18,7 @@ Shared by Qwen3-Omni and Qwen3-TTS talker models.
from __future__ import annotations
import dataclasses
import time
from collections.abc import Iterable, Sequence
import torch
@@ -158,8 +159,8 @@ class CodePredictorAttention(nn.Module):
"""Multi-head self-attention for code predictor.
Uses ``F.scaled_dot_product_attention`` with HF-compatible RoPE and RMSNorm.
No KV cache -- the code predictor always re-prefills the full (short)
sequence each AR step.
The default path re-prefills the short sequence; the opt-in cached path
appends only new positions.
Input : [B, seq_len, hidden_size]
Output: [B, seq_len, hidden_size]
@@ -316,6 +317,53 @@ class CodePredictorAttention(nn.Module):
attn_out = attn_out.transpose(1, 2).reshape(bsz, seq_len, -1)
return self.o_proj(attn_out)
def forward_cached(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
k_cache: torch.Tensor,
v_cache: torch.Tensor,
write_positions: torch.Tensor,
key_mask: torch.Tensor,
) -> torch.Tensor:
"""Attend only the new positions, reusing earlier positions' keys and values.
``k_cache``/``v_cache`` are ``[B, num_kv_heads, max_seq, head_dim]``;
the new positions' keys and values are written at ``write_positions``
(``[n]``). ``key_mask`` (``[1, 1, n, max_seq]``, bool) keeps key
positions up to each query's, so slots not yet written in this call are
never read and every single-position step has the same shapes.
"""
bsz, seq_len, _ = hidden_states.shape
qkv = self.qkv_proj(hidden_states)
q_raw, k_raw, v_raw = self._split_qkv(qkv)
q = self.q_norm(q_raw.view(bsz, seq_len, self.num_heads, self.head_dim)).transpose(1, 2)
k = self.k_norm(k_raw.view(bsz, seq_len, self.num_kv_heads, self.head_dim)).transpose(1, 2)
v = v_raw.view(bsz, seq_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
cos, sin = position_embeddings
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
q = (q * cos) + (_rotate_half(q) * sin)
k = (k * cos) + (_rotate_half(k) * sin)
k_cache.index_copy_(2, write_positions, k.to(k_cache.dtype))
v_cache.index_copy_(2, write_positions, v.to(v_cache.dtype))
if getattr(self, "use_short_kv_attention", False):
from .short_kv_attention import short_kv_attention
attn_out = short_kv_attention(q, k_cache, v_cache, write_positions, self.scaling)
else:
attn_out = F.scaled_dot_product_attention(
q,
k_cache,
v_cache,
attn_mask=key_mask,
scale=self.scaling,
enable_gqa=self.is_gqa,
)
attn_out = attn_out.transpose(1, 2).reshape(bsz, seq_len, -1)
return self.o_proj(attn_out)
# ===================================================================
# MLP
@@ -351,7 +399,7 @@ class CodePredictorMLP(nn.Module):
class CodePredictorDecoderLayer(nn.Module):
"""Transformer decoder layer (SDPA, no KV cache)."""
"""Transformer decoder layer with default re-prefill and opt-in KV reuse."""
def __init__(self, config, *, prefix: str = "") -> None:
super().__init__()
@@ -383,9 +431,29 @@ class CodePredictorDecoderLayer(nn.Module):
hidden_states = residual + hidden_states
return hidden_states
def forward_cached(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
k_cache: torch.Tensor,
v_cache: torch.Tensor,
write_positions: torch.Tensor,
key_mask: torch.Tensor,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn.forward_cached(
hidden_states, position_embeddings, k_cache, v_cache, write_positions, key_mask
)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
return residual + hidden_states
# ===================================================================
# Base Transformer Model (re-prefill, no KV cache)
# Base Transformer Model (re-prefill, or incremental with a K/V buffer)
# ===================================================================
@@ -454,6 +522,29 @@ class CodePredictorBaseModel(nn.Module):
hidden_states = self.norm(hidden_states)
return hidden_states.to(input_dtype)
def forward_cached(
self,
inputs_embeds: torch.Tensor,
position_ids: torch.Tensor,
kv_caches: Sequence[tuple[torch.Tensor, torch.Tensor]],
write_positions: torch.Tensor,
key_mask: torch.Tensor,
) -> torch.Tensor:
"""``forward`` for the new positions only (``position_ids`` ``[B, n]``), reusing earlier K/V."""
input_dtype = inputs_embeds.dtype
use_fp32 = input_dtype == torch.float16 and inputs_embeds.device.type not in ("cpu", "npu")
if use_fp32:
inputs_embeds = inputs_embeds.float()
hidden_states = inputs_embeds
with torch.amp.autocast(inputs_embeds.device.type, enabled=use_fp32, dtype=torch.float32):
position_embeddings = self.rotary_emb(hidden_states, position_ids)
for layer, (k_cache, v_cache) in zip(self.layers, kv_caches):
hidden_states = layer.forward_cached(
hidden_states, position_embeddings, k_cache, v_cache, write_positions, key_mask
)
hidden_states = self.norm(hidden_states)
return hidden_states.to(input_dtype)
# Mapping from HF checkpoint shard name to the (fused parameter,
# slice index) pair used to assemble the fused tensor. See
# ``load_weights`` below.
@@ -626,8 +717,14 @@ class CodePredictorWrapper(nn.Module):
4. torch.compile on inner transformer.
5. Inline sampling (top-k + top-p) -- no custom op overhead.
6. Optional manual CUDA graph capture per batch-size bucket.
``code_predictor_kv_cache`` (CUDA, opt-in) switches to incremental K/V
decoding: at large batches the re-prefill FLOPs are not negligible.
"""
_kv_cache_enabled: bool = False
_fused_sampling_enabled: bool = False
def __init__(
self,
*,
@@ -672,6 +769,7 @@ class CodePredictorWrapper(nn.Module):
# Sampling defaults for "stored" mode
self._top_k: int = 50
self._top_p: float = 0.8
self._do_sample: bool = True
# Lazily initialised state
self._proj_buf: torch.Tensor | None = None
@@ -686,6 +784,7 @@ class CodePredictorWrapper(nn.Module):
# ``None`` keeps the legacy power-of-two bucket derivation.
self._execution_batch_buckets: list[int] | None = None
prefix_graph_cfg = self._stage_connector_extra_config(vllm_config)
self._fused_sampling_enabled = self._parse_bool_config(prefix_graph_cfg.get("code_predictor_fused_sampling"))
prefix_graphs_requested = self._parse_bool_config(prefix_graph_cfg.get("code_predictor_prefix_graphs"))
is_npu = current_omni_platform.is_npu()
# MRv2 captures the whole Talker MTP call. It can therefore capture
@@ -705,15 +804,28 @@ class CodePredictorWrapper(nn.Module):
prefix_graph_cfg.get("code_predictor_prefix_graph_seq_lens")
)
self._prefix_reprefill_seq_lens = tuple(self._prefix_seq_lens(self._num_groups + 1))
# Incremental decoding (opt-in, CUDA): each AR step runs only its new
# position against a static K/V buffer instead of re-prefilling the
# whole prefix (at batch 64 the re-prefill runs 17x the positions).
self._kv_cache_enabled = (
self._parse_bool_config(prefix_graph_cfg.get("code_predictor_kv_cache")) and current_omni_platform.is_cuda()
)
self._short_kv_requested = self._parse_bool_config(prefix_graph_cfg.get("code_predictor_short_kv"))
self._short_kv_enabled = False
self._kv_buf: torch.Tensor | None = None
self._compiled_kv_fwd = None
# Per AR step: (new positions [n], key mask [1, 1, n, max_seq]).
self._kv_steps: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
def get_input_embeddings(self) -> nn.ModuleList:
return self.model.get_input_embeddings()
def set_sampling_params(self, top_k: int = 50, top_p: float = 0.8) -> None:
"""Configure sampling parameters to maintain consistency with previous implementation."""
def set_sampling_params(self, top_k: int = 50, top_p: float = 0.8, do_sample: bool = True) -> None:
"""Configure "stored"-mode sampling; ``do_sample=False`` selects greedy argmax."""
self._top_k = top_k
self._top_p = top_p
logger.debug("Sampling parameters updated: top_k=%d, top_p=%.2f", top_k, top_p)
self._do_sample = do_sample
logger.debug("Sampling parameters updated: top_k=%d, top_p=%.2f, do_sample=%s", top_k, top_p, do_sample)
# ------------------------------------------------------------------
# Lazy-init helpers
@@ -730,6 +842,35 @@ class CodePredictorWrapper(nn.Module):
):
return
self._proj_buf = torch.zeros(bsz, max_seq, self._cp_hidden, dtype=dtype, device=device)
if self._kv_cache_enabled:
cfg = self.config
head_dim = int(getattr(cfg, "head_dim", None) or cfg.hidden_size // cfg.num_attention_heads)
self._kv_buf = torch.zeros(
int(cfg.num_hidden_layers),
2,
bsz,
int(cfg.num_key_value_heads),
max_seq,
head_dim,
dtype=dtype,
device=device,
)
def _kv_views(self, bsz: int) -> list[tuple[torch.Tensor, torch.Tensor]]:
kv_buf = self._kv_buf
assert kv_buf is not None
return [(kv_buf[i, 0, :bsz], kv_buf[i, 1, :bsz]) for i in range(kv_buf.shape[0])]
def _ensure_kv_steps(self, device: torch.device) -> None:
if self._kv_steps:
return
max_seq = self._num_groups + 1
keys = torch.arange(max_seq, device=device)
for step in range(1, self._num_groups):
# Step 1 runs positions 0 (talker hidden) and 1 (CB0); step s > 1 runs position s.
positions = torch.arange(0, 2, device=device) if step == 1 else torch.tensor([step], device=device)
key_mask = (keys[None, :] <= positions[:, None]).view(1, 1, positions.numel(), max_seq)
self._kv_steps[step] = (positions, key_mask)
def _setup_compile(self) -> None:
"""Lazily set up torch.compile with optional device graph capture."""
@@ -742,6 +883,9 @@ class CodePredictorWrapper(nn.Module):
self._model_dtype = next(self.model.parameters()).dtype
self._lm_heads_list = list(self.lm_head)
self._codec_embeds_list = list(self.model.codec_embedding)
if self._model_dtype == torch.float16 or not current_omni_platform.supports_torch_inductor():
# The fp16 body runs in fp32 under autocast; keep re-prefill there.
self._kv_cache_enabled = False
# Torch 2.13 XPU Dynamo can double-register built-in handlers when
# spawned workers compile this predictor. Keep this narrow path eager
@@ -768,10 +912,28 @@ class CodePredictorWrapper(nn.Module):
dynamic=False,
options={"epilogue_fusion": False},
)
self._short_kv_enabled = (
self._kv_cache_enabled
and self._short_kv_requested
and current_omni_platform.is_cuda()
and self._model_dtype == torch.bfloat16
)
for layer in self.model.layers:
layer.self_attn.use_short_kv_attention = self._short_kv_enabled
if self._kv_cache_enabled:
self._compiled_kv_fwd = torch.compile(
self.model.forward_cached,
dynamic=self._short_kv_enabled,
options={"epilogue_fusion": False},
)
with torch._dynamo.config.patch(cache_size_limit=self._compile_cache_size_limit()):
self._warmup_buckets()
if self._wrapper_config.use_cuda_graphs:
if self._kv_cache_enabled:
# Fifteen short steps per call: they rely on the runner capturing
# the whole MTP call (MRv2) rather than on per-step inner graphs.
logger.info("code_predictor: torch.compile + incremental K/V decoding")
elif self._wrapper_config.use_cuda_graphs:
self._capture_cuda_graphs()
logger.info("code_predictor: torch.compile (no epilogue fusion) + CUDA graphs")
else:
@@ -792,7 +954,13 @@ class CodePredictorWrapper(nn.Module):
extra_cfg = connector_cfg.get("extra", connector_cfg)
else:
extra_cfg = getattr(connector_cfg, "extra", None)
return extra_cfg if isinstance(extra_cfg, dict) else {}
extra_cfg = extra_cfg if isinstance(extra_cfg, dict) else {}
# A single-stage deployment has no connector edge to carry model
# options; it sets them in the stage's ``additional_config``.
additional = getattr(vllm_config, "additional_config", None)
if isinstance(additional, dict) and additional:
return {**additional, **extra_cfg}
return extra_cfg
@staticmethod
def _parse_bool_config(value: object) -> bool:
@@ -840,13 +1008,13 @@ class CodePredictorWrapper(nn.Module):
return row_generators
@classmethod
def _sample_codes_gumbel(
def _sampling_uniforms(
cls,
logits: torch.Tensor,
generator: _GeneratorLike = None,
uniforms: torch.Tensor | None = None,
) -> torch.Tensor:
"""Sample ``logits`` via Gumbel-max with optional precomputed noise."""
"""Draw one FP32 uniform per original token, preserving generator state."""
if uniforms is None:
row_generators = cls._normalize_generators(generator, int(logits.shape[0]))
u = torch.empty_like(logits, dtype=torch.float32)
@@ -862,6 +1030,17 @@ class CodePredictorWrapper(nn.Module):
f"logits={tuple(logits.shape)}"
)
u = uniforms
return u
@classmethod
def _sample_codes_gumbel(
cls,
logits: torch.Tensor,
generator: _GeneratorLike = None,
uniforms: torch.Tensor | None = None,
) -> torch.Tensor:
"""Sample ``logits`` via Gumbel-max with optional precomputed noise."""
u = cls._sampling_uniforms(logits, generator, uniforms)
return (logits.float() - torch.log(-torch.log(u))).argmax(dim=-1, keepdim=True)
def _prefix_seq_lens(self, max_seq: int) -> list[int]:
@@ -924,6 +1103,8 @@ class CodePredictorWrapper(nn.Module):
if needs_full_graph:
full_graph_entries += len(prefix_buckets)
required_entries = full_graph_entries + len(prefix_buckets) * len(prefix_seq_lens)
if self._kv_cache_enabled:
required_entries = 4 # {first step, later steps} x {batch 1, dynamic batch}
return max(torch._dynamo.config.cache_size_limit, required_entries)
def _warmup_buckets(self) -> None:
@@ -938,7 +1119,29 @@ class CodePredictorWrapper(nn.Module):
self._ensure_buffers(device, self._model_dtype, max(self._bucket_sizes))
proj_buf = self._proj_buf
if self._prefix_reprefill_enabled:
if self._kv_cache_enabled:
self._ensure_kv_steps(device)
started = time.perf_counter()
for bsz in self._bucket_sizes:
caches = self._kv_views(bsz)
for step in (1, 2):
positions, key_mask = self._kv_steps[step]
lo = 0 if step == 1 else step
hidden = proj_buf[:bsz, lo : step + 1, :]
pos_ids = positions.unsqueeze(0).expand(bsz, -1)
if bsz > 1:
# One graph for every batch >= 2 per step shape (4 compiles, not 2 per bucket):
# a cold per-bucket compile outlasted the stage handshake timeout.
for tensor in (hidden, pos_ids, *(t for kv in caches for t in kv)):
torch._dynamo.mark_dynamic(tensor, 0)
for _ in range(2):
self._compiled_kv_fwd(hidden, pos_ids, caches, positions, key_mask)
logger.info(
"code_predictor: incremental K/V decoding warmed for buckets %s in %.1f s",
self._bucket_sizes,
time.perf_counter() - started,
)
elif self._prefix_reprefill_enabled:
prefix_seq_lens = self._prefix_reprefill_seq_lens
needs_full_graph = set(prefix_seq_lens) != set(range(2, max_seq))
for bsz in self._bucket_sizes:
@@ -1135,6 +1338,21 @@ class CodePredictorWrapper(nn.Module):
scaled = scaled.masked_fill(scaled < topk_vals[:, -1:], float("-inf"))
return self._sample_codes_gumbel(scaled, generator=generator, uniforms=uniforms)
def _validate_sampling_inputs(
self,
bsz: int,
generators: Sequence[torch.Generator | None] | None,
sample_uniforms: torch.Tensor | None,
) -> None:
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}")
if sample_uniforms is not None:
expected_shape = (bsz, self._num_groups - 1, int(self.config.vocab_size))
if tuple(sample_uniforms.shape) != expected_shape:
raise ValueError(
f"sample_uniforms must have shape {expected_shape}, got {tuple(sample_uniforms.shape)}"
)
@torch.inference_mode()
def forward(
self,
@@ -1152,14 +1370,7 @@ class CodePredictorWrapper(nn.Module):
"""Predict residual codebooks 1..G-1 autoregressively via re-prefill."""
bsz = int(layer0_code.shape[0])
num_groups = self._num_groups
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}")
if sample_uniforms is not None:
expected_shape = (bsz, num_groups - 1, int(self.config.vocab_size))
if tuple(sample_uniforms.shape) != expected_shape:
raise ValueError(
f"sample_uniforms must have shape {expected_shape}, got {tuple(sample_uniforms.shape)}"
)
self._validate_sampling_inputs(bsz, generators, sample_uniforms)
sample_generator: _GeneratorLike = generators if generators is not None else generator
device = layer0_code.device
@@ -1206,9 +1417,29 @@ class CodePredictorWrapper(nn.Module):
all_codes = torch.empty(bsz, num_groups, dtype=torch.long, device=device)
all_codes[:, 0] = layer0_code.reshape(bsz)
kv_caches = None
if self._kv_cache_enabled:
# Masked slots still enter the attention matmul with zero weight, so
# clear them: a non-finite value left by an earlier call would leak.
if not self._short_kv_enabled:
self._kv_buf[:, :, :padded_bsz].zero_()
kv_caches = self._kv_views(padded_bsz)
# Autoregressive loop: predict layers 1..G-1
for step in range(1, num_groups):
logits = self._predict_step_logits(proj_buf, bsz, padded_bsz, step, is_npu_capturing)
if kv_caches is not None:
# Step 1 runs positions 0-1; later steps append one position.
positions, key_mask = self._kv_steps[step]
lo = 0 if step == 1 else step
new_hidden = self._compiled_kv_fwd(
proj_buf[:padded_bsz, lo : step + 1, :],
positions.unsqueeze(0).expand(padded_bsz, -1),
kv_caches,
positions,
key_mask,
)
logits = self._lm_heads_list[step - 1](new_hidden[:bsz, -1, :])
else:
logits = self._predict_step_logits(proj_buf, bsz, padded_bsz, step, is_npu_capturing)
# Sample next code via Gumbel-max.
#
@@ -1222,7 +1453,15 @@ class CodePredictorWrapper(nn.Module):
# 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:
if stored_mode and not self._do_sample:
code = logits.argmax(dim=-1, keepdim=True)
elif stored_mode and self._fused_sampling_enabled and logits.is_cuda and torch.version.hip is None:
from .fused_sampling import sample_top_k_top_p_gumbel
step_uniforms = sample_uniforms[:, step - 1, :] if sample_uniforms is not None else None
uniforms = self._sampling_uniforms(logits, sample_generator, step_uniforms)
code = sample_top_k_top_p_gumbel(logits, uniforms, top_k=s_top_k, top_p=s_top_p)
elif stored_mode:
# "stored" mode: top-k -> top-p -> Gumbel-max
if s_top_k > 0:
topk_vals, _ = logits.topk(s_top_k, dim=-1)
@@ -0,0 +1,29 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Shared eligibility; each model separately controls its first-audio opt-in."""
from typing import Any
import torch
from vllm_omni.platforms import current_omni_platform
def supports_talker_first_audio(vllm_config: Any) -> bool:
"""Require CUDA MRv2 streaming, local single-rank execution and no prefix replay.
Other configurations retain the regular codec path. These conditions do
not enable the feature: model-specific options must also accept it.
"""
model = vllm_config.model_config
if not (getattr(model, "use_v2_model_runner", False) and getattr(model, "async_chunk", False)):
return False
parallel = vllm_config.parallel_config
return (
current_omni_platform.is_cuda()
and torch.device(vllm_config.device_config.device).type == "cuda"
and parallel.tensor_parallel_size == 1
and parallel.pipeline_parallel_size == 1
and parallel.distributed_executor_backend in (None, "uni")
and not vllm_config.cache_config.enable_prefix_caching
)
@@ -966,7 +966,20 @@ class CausalHiFTGenerator(HiFTGenerator):
self.conv_pre_look_right = conv_pre_look_right
self.f0_predictor = f0_predictor
def enable_decode_graphs(self) -> None:
"""Opt into exact-shape CUDA replay after frozen weights are loaded."""
from .hift_graph import HiFTDecodeGraphs
self.enable_cached_istft()
self._decode_graphs = HiFTDecodeGraphs(self._decode_eager)
def decode(self, x: torch.Tensor, s: torch.Tensor = torch.zeros(1, 1, 0), finalize: bool = True) -> torch.Tensor:
runner = getattr(self, "_decode_graphs", None)
if runner is not None:
return runner.run(x, s, finalize)
return self._decode_eager(x, s, finalize=finalize)
def _decode_eager(self, x: torch.Tensor, s: torch.Tensor, finalize: bool = True) -> torch.Tensor:
s_stft_real, s_stft_imag = self._stft(s.squeeze(1))
if finalize is True:
x = self.conv_pre(x)
@@ -0,0 +1,85 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Bounded CUDA graph replay for HiFT's deterministic waveform decoder.
F0, sine/noise generation, phase carry, and per-request streaming state remain
outside the graph. Each replay consumes current mel and source tensors and
returns an owned waveform, because multiple requests can reuse one shape.
"""
from collections import OrderedDict
from collections.abc import Callable
from dataclasses import dataclass
import torch
@dataclass
class _DecodeGraph:
graph: torch.cuda.CUDAGraph
mel: torch.Tensor
source: torch.Tensor
output: torch.Tensor
class HiFTDecodeGraphs:
"""Capture recurrent exact shapes, with at most 32 resident graphs.
``decode`` must have warmed its ISTFT envelope before capture and remain
deterministic for its mel, source and finalize arguments. Uncommon shapes,
CPU execution and nested CUDA capture use that same eager implementation.
"""
def __init__(self, decode: Callable[..., torch.Tensor], max_graphs: int = 32) -> None:
self.decode = decode
self.max_graphs = max_graphs
self.graphs: dict[tuple, _DecodeGraph] = {}
self.seen: OrderedDict[tuple, int] = OrderedDict()
self.pool = None
@torch.inference_mode()
def run(self, mel: torch.Tensor, source: torch.Tensor, finalize: bool) -> torch.Tensor:
"""Replay a recurring layout without aliasing another request's output."""
if not mel.is_cuda or not source.is_cuda or torch.cuda.is_current_stream_capturing():
return self.decode(mel, source, finalize=finalize)
autocast = torch.is_autocast_enabled("cuda")
key = (
mel.shape,
source.shape,
mel.device,
mel.dtype,
source.dtype,
finalize,
torch.backends.cudnn.allow_tf32,
torch.backends.cuda.matmul.allow_tf32,
autocast,
torch.get_autocast_dtype("cuda") if autocast else None,
)
state = self.graphs.get(key)
if state is None:
if len(self.graphs) >= self.max_graphs:
return self.decode(mel, source, finalize=finalize)
count = self.seen.pop(key, 0) + 1
self.seen[key] = count
if len(self.seen) > 128:
self.seen.popitem(last=False)
if count < 3:
return self.decode(mel, source, finalize=finalize)
static_mel, static_source = mel.clone(), source.clone()
stream = torch.cuda.Stream(device=mel.device)
stream.wait_stream(torch.cuda.current_stream(mel.device))
with torch.cuda.stream(stream):
self.decode(static_mel, static_source, finalize=finalize)
torch.cuda.current_stream(mel.device).wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, pool=self.pool):
output = self.decode(static_mel, static_source, finalize=finalize)
if self.pool is None:
self.pool = graph.pool()
state = _DecodeGraph(graph, static_mel, static_source, output)
self.graphs[key] = state
state.mel.copy_(mel)
state.source.copy_(source)
state.graph.replay()
# A later request may replay this shape before the caller consumes it.
return state.output.clone()
@@ -170,7 +170,7 @@ class PackedDiT:
t,
rows,
attention,
max_seqlen_q=attention.page_table.shape[1],
max_seqlen_q=attention.max_seqlen_q,
block_modulation=block_modulation,
final_modulation=final_modulation,
rope=rope,
@@ -771,7 +771,9 @@ class FlowGraphRunner:
attention.heads, attention.head_dim = self.heads, self.head_dim
attention.cache_seqlens, attention.cu_seqlens_q = state.cache_seqlens, state.cu_seqlens_q
attention.page_table = state.page_table
attention.max_seqlen_q = state.page_table.shape[1]
# Streaming queries cover one training chunk, even when their KV
# prefix and the packed graph allocation contain thousands of frames.
attention.max_seqlen_q = estimator.dit.static_chunk_size if key[0] else state.page_table.shape[1]
mark_packed_compile_metadata(rows, attention)
mu_cfg = torch.cat((state.mu, torch.zeros_like(state.mu)), dim=1)
cond_cfg = torch.cat((state.cond, torch.zeros_like(state.cond)), dim=1)
@@ -769,4 +769,11 @@ class CosyVoice3Code2Wav(nn.Module):
if f0_folded == 0:
logger.warning("HiFT F0 predictor had no weight-norm layers to fold; check config drift")
self.hift.eval()
if device.type == "cuda" and cosyvoice3_packed_streaming_enabled():
if os.getenv("COSYVOICE3_CACHED_ISTFT", "0") == "1":
self.hift.enable_cached_istft()
logger.info("CosyVoice3 streaming HiFT: cached ISTFT enabled")
if os.getenv("COSYVOICE3_HIFT_GRAPH", "0") == "1":
self.hift.enable_decode_graphs()
logger.info("CosyVoice3 streaming HiFT: decode CUDA graphs enabled")
logger.info(f"Loaded hift weights from {hift_path}")
@@ -539,11 +539,15 @@ class RingKVCache:
end_offset = self.end_offset.index_select(0, slots)
row_cache = self.cache.index_select(1, slots)
indexes = torch.arange(T, device=end_offset.device, dtype=end_offset.dtype)
# Upsampling can make a chunk longer than the ring. Only the last
# capacity tokens survive; writing all T tokens gives scatter_
# duplicate destinations and nondeterministic cache contents.
write_length = min(T, self.capacity)
indexes = torch.arange(T - write_length, T, device=end_offset.device, dtype=end_offset.dtype)
indexes = (indexes + end_offset.view(-1, 1)) % self.capacity
scatter_indexes = indexes.view(B, 1, T, 1).expand(-1, H, T, D)
row_cache[0].scatter_(2, scatter_indexes, k)
row_cache[1].scatter_(2, scatter_indexes, v)
scatter_indexes = indexes.view(B, 1, write_length, 1).expand(-1, H, write_length, D)
row_cache[0].scatter_(2, scatter_indexes, k[:, :, -write_length:])
row_cache[1].scatter_(2, scatter_indexes, v[:, :, -write_length:])
# Live and graph-padding rows always map to distinct slots. The
# latter map only to scratch state, so this write cannot corrupt a
# request even though dense graph operators still execute it.
@@ -729,7 +733,13 @@ class MossAudioTokenizerMultiheadAttention(StreamingModule):
if self.context is None:
capacity = self.weights_per_step if self.weights_per_step else 1024
else:
capacity = self.context
# RingKVCache.complete writes a whole chunk before attending, so a
# ring of exactly ``context`` entries lets a T-token chunk evict up
# to T tokens of history that the chunk's own queries should still
# see (and, for T > context, evict part of the chunk itself).
# ``_ring_headroom`` (max chunk tokens at this layer) keeps every
# in-window key resident; 0 preserves the legacy truncation.
capacity = self.context + int(getattr(self, "_ring_headroom", 0))
kv_cache = RingKVCache(
batch_size,
@@ -791,29 +801,63 @@ class MossAudioTokenizerMultiheadAttention(StreamingModule):
if self.rope:
q, k = self.rope(q, k, offset, time_before_heads=False)
k, v, pos_k = self._complete_kv(k, v, execution_context)
pos_k = pos_k[:, None]
if self.causal:
pos_q = offset.view(-1, 1, 1) + torch.arange(T, device=q.device, dtype=torch.long).view(-1, 1)
delta = pos_q - pos_k
attn_bias = (pos_k >= 0) & (delta >= 0)
if self.context is not None:
attn_bias = attn_bias & (delta < self.context)
attn_bias = attn_bias[:, None]
slot_attention = getattr(self, "_slot_attention", None)
slot_attention_rows = getattr(self, "_slot_attention_rows", None)
advance_kv_offset = False
slot_path = state is not None and execution_context is not None and self.causal
if slot_path and slot_attention_rows is not None:
# One launch: the gathered MHA offset equals the ring's end_offset
# for every slot, and both advance together below.
valid_lengths = execution_context.valid_rows.to(dtype=torch.int32) * T
x = slot_attention_rows(
q,
k,
v,
state.kv_cache.cache,
offset,
execution_context.state_slot_ids,
valid_lengths,
self.context if self.context is not None else -1,
)
advance_kv_offset = True
elif slot_attention is not None and slot_path:
# Current streaming batches use one exact T. Padding rows own
# scratch slots and must not advance persistent attention state.
valid_lengths = execution_context.valid_rows.to(dtype=torch.int32) * T
x = slot_attention(
q,
k,
v,
state.kv_cache.cache,
state.kv_cache.end_offset,
execution_context.state_slot_ids,
valid_lengths,
self.context if self.context is not None else -1,
)
else:
attn_bias = None
k, v, pos_k = self._complete_kv(k, v, execution_context)
pos_k = pos_k[:, None]
streaming_attention = getattr(self, "_streaming_attention", None)
if (
streaming_attention is not None
and attn_bias is not None
and q.dtype == torch.bfloat16
and q.shape[-1] == 64
):
x = streaming_attention(q, k, v, attn_bias)
else:
x = F.scaled_dot_product_attention(q, k, v, attn_bias, dropout_p=0.0)
if self.causal:
pos_q = offset.view(-1, 1, 1) + torch.arange(T, device=q.device, dtype=torch.long).view(-1, 1)
delta = pos_q - pos_k
attn_bias = (pos_k >= 0) & (delta >= 0)
if self.context is not None:
attn_bias = attn_bias & (delta < self.context)
attn_bias = attn_bias[:, None]
else:
attn_bias = None
streaming_attention = getattr(self, "_streaming_attention", None)
if (
streaming_attention is not None
and attn_bias is not None
and q.dtype == torch.bfloat16
and q.shape[-1] == 64
):
x = streaming_attention(q, k, v, attn_bias)
else:
x = F.scaled_dot_product_attention(q, k, v, attn_bias, dropout_p=0.0)
x = x.transpose(1, 2).reshape(B, T, self.embed_dim)
x = apply_weights_per_step(self.out_projs, self.weights_per_step_schedule, x, offset_cpu)
@@ -824,6 +868,8 @@ class MossAudioTokenizerMultiheadAttention(StreamingModule):
else:
next_offset = torch.where(execution_context.valid_rows, offset + T, offset)
state.offset.index_copy_(0, execution_context.state_slot_ids, next_offset)
if advance_kv_offset:
state.kv_cache.end_offset.index_copy_(0, execution_context.state_slot_ids, next_offset)
return x
@@ -1624,6 +1670,7 @@ class MossAudioTokenizerModel(MossAudioTokenizerPreTrainedModel):
decoder_kwargs_list = copy.deepcopy(config.decoder_kwargs)
self.decoder = nn.ModuleList()
decoder_input_frame_rate = current_frame_rate
for decoder_kwargs_i in decoder_kwargs_list:
decoder_kwargs_i = dict(decoder_kwargs_i)
if decoder_kwargs_i["module_type"] == "PatchedPretransform":
@@ -1638,6 +1685,11 @@ class MossAudioTokenizerModel(MossAudioTokenizerPreTrainedModel):
context=int(round(current_frame_rate * context_duration)),
)
)
# Tokens this transformer sees per input code frame. Streaming
# ring headroom is sized from it (see initialize_decoder_state_pool).
self.decoder[-1].tokens_per_input_frame = max(
1, int(round(current_frame_rate / decoder_input_frame_rate))
)
current_frame_rate *= self.decoder[-1].downsample_ratio
expected_output_frame_rate = float(self.sampling_rate * channel_interleave_factor)
@@ -1731,10 +1783,29 @@ class MossAudioTokenizerModel(MossAudioTokenizerPreTrainedModel):
self._decoder_state_capacity = 0
self._decoder_slot_offsets = None
def initialize_decoder_state_pool(self, state_capacity: int, scratch_capacity: int = 0) -> None:
"""Allocate persistent decoder state independently of execution B."""
if state_capacity <= 0 or scratch_capacity < 0:
raise ValueError(f"Invalid decoder state capacities: state={state_capacity}, scratch={scratch_capacity}.")
def initialize_decoder_state_pool(
self, state_capacity: int, scratch_capacity: int = 0, chunk_frames: int = 0
) -> None:
"""Allocate persistent decoder state independently of execution B.
``chunk_frames`` > 0 sizes every decoder attention ring as
``context + chunk_frames * tokens_per_input_frame`` so that a chunk of
up to ``chunk_frames`` code frames never evicts keys inside its own
causal window; chunked streaming then matches whole-sequence decoding.
``chunk_frames`` = 0 keeps the legacy ``context``-sized rings.
"""
if state_capacity <= 0 or scratch_capacity < 0 or chunk_frames < 0:
raise ValueError(
"Invalid decoder state capacities: "
f"state={state_capacity}, scratch={scratch_capacity}, chunk_frames={chunk_frames}."
)
for module in self.decoder:
if not isinstance(module, MossAudioTokenizerProjectedTransformer):
continue
tokens_per_frame = int(getattr(module, "tokens_per_input_frame", 1))
for attention in module.modules():
if isinstance(attention, MossAudioTokenizerMultiheadAttention):
attention._ring_headroom = chunk_frames * tokens_per_frame
self._start_streaming(state_capacity + scratch_capacity, decoder_only=True)
self._decoder_state_capacity = state_capacity + scratch_capacity
@@ -81,15 +81,31 @@ class CUDAGraphStreamingDecoderWrapper:
frame_sizes: list[int],
num_quantizers: int,
vllm_config: VllmConfig,
scratch_base: int | None = None,
private_pool: bool = False,
compiled_decode: nn.Module | None = None,
) -> None:
self.codec = codec
self.state_capacity = int(state_capacity)
# Graph padding rows use scratch slots [scratch_base, scratch_base+B).
# A wrapper replaying concurrently with another needs its own scratch
# range and graph memory pool.
self.scratch_base = self.state_capacity if scratch_base is None else int(scratch_base)
self._private_pool = bool(private_pool)
# Graphs bake in the cuBLAS workspace of the stream they were captured
# on, and torch.cuda.graph shares one default capture stream. Split-K
# GEMMs synchronize through that workspace, so two graphs replaying
# concurrently from one capture stream can spin on each other forever.
self._capture_stream: torch.cuda.Stream | None = None
self.batch_sizes = sorted({int(size) for size in batch_sizes if 0 < int(size) <= state_capacity})
self.frame_sizes = sorted({int(size) for size in frame_sizes if int(size) > 0})
self.num_quantizers = int(num_quantizers)
self.graphs: dict[tuple[int, int], _CapturedStreamingDecodeGraph] = {}
self._pool = None
self._warmed_up = False
if compiled_decode is not None:
self._compiled_decode: nn.Module | None = compiled_decode
return
# vLLM owns Inductor compilation; this wrapper remains the sole owner
# of CUDA Graph capture/replay because it understands persistent codec
# state slots. Disable vLLM's CUDA Graph layer to avoid nested graphs.
@@ -98,7 +114,7 @@ class CUDAGraphStreamingDecoderWrapper:
compile_config.compilation_config.cudagraph_mode = CUDAGraphMode.NONE
compile_config.compilation_config.static_forward_context = {}
with set_current_vllm_config(compile_config):
self._compiled_decode: nn.Module | None = _MossStreamingDecodeCompileAdapter(
self._compiled_decode = _MossStreamingDecodeCompileAdapter(
codec,
vllm_config=compile_config,
)
@@ -175,7 +191,7 @@ class CUDAGraphStreamingDecoderWrapper:
frame_size,
exc_info=True,
)
scratch_slots = self.state_capacity + torch.arange(
scratch_slots = self.scratch_base + torch.arange(
batch_size,
dtype=torch.long,
device=device,
@@ -206,10 +222,13 @@ class CUDAGraphStreamingDecoderWrapper:
device=device,
)
lengths = torch.zeros(batch_size, dtype=torch.long, device=device)
scratch_slots = self.state_capacity + torch.arange(batch_size, dtype=torch.long, device=device)
scratch_slots = self.scratch_base + torch.arange(batch_size, dtype=torch.long, device=device)
valid_rows = torch.zeros(batch_size, dtype=torch.bool, device=device)
stream = torch.cuda.Stream()
if self._private_pool and self._capture_stream is None:
self._capture_stream = torch.cuda.Stream(device=device)
# Warm up on the capture stream so its workspace exists before capture.
stream = self._capture_stream or torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
@@ -219,9 +238,11 @@ class CUDAGraphStreamingDecoderWrapper:
self.codec.reset_decoder_state_slots(scratch_slots)
if self._pool is None:
self._pool = current_platform.get_global_graph_pool()
self._pool = (
torch.cuda.graph_pool_handle() if self._private_pool else current_platform.get_global_graph_pool()
)
graph = CUDAGraph()
with torch.cuda.graph(graph, pool=self._pool, capture_error_mode="thread_local"):
with torch.cuda.graph(graph, pool=self._pool, stream=self._capture_stream, capture_error_mode="thread_local"):
audio, audio_lengths = decode(codes, lengths, scratch_slots, valid_rows)
self.graphs[(batch_size, frame_size)] = _CapturedStreamingDecodeGraph(
@@ -272,7 +293,7 @@ class CUDAGraphStreamingDecoderWrapper:
entry.static_lengths.zero_()
entry.static_lengths[:actual_batch_size].fill_(int(frame_size))
entry.static_state_slot_ids.copy_(
self.state_capacity + torch.arange(batch_size, dtype=torch.long, device=entry.static_state_slot_ids.device)
self.scratch_base + torch.arange(batch_size, dtype=torch.long, device=entry.static_state_slot_ids.device)
)
entry.static_state_slot_ids[:actual_batch_size].copy_(state_slot_ids, non_blocking=True)
entry.static_valid_rows.zero_()
@@ -0,0 +1,333 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Decode a stream's first codec chunk as soon as Stage 1 receives it.
The Stage-1 engine loop only sees a new chunk between steps, and a step waits
for its codec graph to finish. A first chunk that arrives mid-step therefore
waits for the rest of that step, then for its own replay. This path takes the
first chunk from the receive thread, replays a small dedicated graph on its
own stream while the main step runs, and emits the audio straight to the
engine output queue. The main path later receives the same chunk, finds it
already decoded, and continues the stream from the state written here.
"""
from __future__ import annotations
import math
import queue
import threading
import time
from dataclasses import dataclass
from typing import Any
import torch
from vllm.logger import init_logger
from vllm_omni.platforms import current_omni_platform
from vllm_omni.worker_v2.first_audio_sender import FirstAudioSink, _PreparedDelivery
logger = init_logger(__name__)
class _SlotHandoff:
"""Orders the main path's first use of a slot after the fast decode."""
__slots__ = ("done", "event", "error")
def __init__(self) -> None:
self.done = threading.Event()
self.event: torch.cuda.Event | None = None
self.error: Exception | None = None
@dataclass
class _Job:
request_id: str
request_key: str
delivery: _PreparedDelivery
slot: int
codes: torch.Tensor # (n_vq, T) int64 on host
handoff: _SlotHandoff
class MossFirstChunkFastPath:
def __init__(
self,
session: Any,
wrapper: Any,
*,
n_vq: int,
frames: int,
codebook_size: int,
samples_per_frame: int,
n_channels: int,
sample_rate: torch.Tensor,
device: torch.device,
gate_main: bool = False,
handoff_timeout_s: float = 30.0,
) -> None:
if not math.isfinite(handoff_timeout_s) or handoff_timeout_s <= 0:
raise ValueError("handoff_timeout_s must be positive and finite")
self._handoff_timeout_s = float(handoff_timeout_s)
self._session = session
# When set, the main codec stream waits for an in-flight fast decode
# before its next replay, so the small first-chunk graph runs mostly
# uncontended instead of interleaving with a large regular replay.
self._gate_main = bool(gate_main)
self._inflight: torch.cuda.Event | None = None
self._wrapper = wrapper
self._n_vq = int(n_vq)
self.frames = int(frames)
self._codebook_size = int(codebook_size)
self._samples = int(frames) * int(samples_per_frame)
self._n_channels = int(n_channels)
self._sr = sample_rate
if device.index is None:
device = torch.device(device.type, torch.accelerator.current_device_index())
self._device = device
self._max_batch = max(wrapper.batch_sizes)
self._stream = torch.cuda.Stream(device=device, priority=-1)
self._host_codes = torch.zeros((self._n_vq, self._max_batch, self.frames), dtype=torch.long).pin_memory()
self._host_slots = torch.zeros(self._max_batch, dtype=torch.long).pin_memory()
audio_shape = (self._max_batch, self._n_channels, self._samples)
self._host_audio = torch.empty(audio_shape, dtype=torch.float32).pin_memory()
self._copied = torch.cuda.Event()
self._sink: FirstAudioSink | None = None
# Request key -> slot for chunks decoded here and not yet seen by the main path.
self._decoded: dict[str, int] = {}
# A failed output-queue handoff must be replayed from the already
# decoded samples. Re-decoding would advance the streaming state twice.
self._unsent_audio: dict[str, torch.Tensor] = {}
self._handoffs: dict[int, _SlotHandoff] = {}
self._lock = threading.Lock()
self._jobs: queue.SimpleQueue[_Job | None] = queue.SimpleQueue()
self._thread: threading.Thread | None = None
self._closed = False
self._worker_error: Exception | None = None
self._emitted = 0
self._batches = 0
self._decode_s = 0.0
def bind(self, sink: FirstAudioSink) -> None:
if not callable(getattr(sink, "prepare", None)):
raise TypeError("The codec first-chunk sink requires prepare(request_ids)")
with self._lock:
if self._closed:
raise RuntimeError("The codec first-chunk decoder is closed")
self._sink = sink
if self._thread is None:
self._thread = threading.Thread(target=self._run, name="moss-codec-first-chunk", daemon=True)
self._thread.start()
def close(self) -> None:
with self._lock:
if not self._closed:
self._closed = True
self._jobs.put(None)
thread = self._thread
if thread is not None:
thread.join(timeout=5.0)
# A stalled decode may still own GPU state: keep its thread and
# handoff, rather than permitting a restart or slot reuse.
if thread.is_alive():
logger.warning("Codec first-chunk decoder is still stopping; slot waits remain bounded")
# ------------------------------------------------------------------
# Receive thread
# ------------------------------------------------------------------
def submit(
self,
request_id: str,
request_key: str,
codes_flat: Any,
req_slots: dict[str, int],
) -> bool:
"""Claim a first chunk for fast decode; False leaves it to the main path."""
# Own the codes: the job outlives this call and the payload it came from.
codes = torch.as_tensor(codes_flat, dtype=torch.long).reshape(-1).clone()
if codes.numel() != self._n_vq * self.frames:
return False
with self._lock:
if (
self._closed
or self._sink is None
or self._thread is None
or not self._thread.is_alive()
or request_key in req_slots
):
return False
delivery = self._sink.prepare([request_id])
if request_id not in delivery.routes:
return False
slot = self._session.acquire(oldest=True)
if slot is None:
return False
handoff = _SlotHandoff()
self._handoffs[slot] = handoff
self._decoded[request_key] = slot
req_slots[request_key] = slot
codes = codes.reshape(self._n_vq, self.frames)
# Enqueue under the admission lock: close's sentinel cannot
# overtake an accepted job.
self._jobs.put(_Job(request_id, request_key, delivery, slot, codes, handoff))
return True
# ------------------------------------------------------------------
# Main (engine) thread
# ------------------------------------------------------------------
def get_request_slot(self, request_key: str, req_slots: dict[str, int]) -> int | None:
"""Observe a completed admission before deciding whether cleanup is needed.
The scheduler removes cancelled routes before the runner's finish hook.
A submit that already froze its route must finish leasing its slot
before that hook checks it. Waiting for decode happens outside this
lock, so the worker can still complete the slot handoff.
"""
with self._lock:
return req_slots.get(request_key)
def take_decoded(self, request_key: str) -> bool:
"""True once per request whose first chunk this path already decoded."""
with self._lock:
return self._decoded.pop(request_key, None) is not None
def take_unsent_audio(self, request_key: str) -> torch.Tensor | None:
"""Return first-chunk samples when the out-of-band sink rejected them."""
with self._lock:
return self._unsent_audio.pop(request_key, None)
def forget(self, request_key: str) -> None:
with self._lock:
self._decoded.pop(request_key, None)
self._unsent_audio.pop(request_key, None)
def gate(self) -> None:
"""Queue the current stream behind the latest fast decode, if still running."""
event = self._inflight
if self._gate_main and event is not None and not event.query():
torch.cuda.current_stream(self._device).wait_event(event)
def order_after(self, slot: int) -> None:
"""Make the current stream's next use of ``slot`` follow the fast decode."""
with self._lock:
handoff = self._handoffs.get(slot)
if handoff is None:
return
if not handoff.done.wait(timeout=self._handoff_timeout_s):
# Keep the handoff. The worker may still be writing the slot, so
# neither cancellation nor a retry may release/re-decode it.
raise TimeoutError(f"Codec first-chunk slot {slot} did not complete within {self._handoff_timeout_s:g}s")
if handoff.error is not None:
raise RuntimeError(f"Codec first-chunk decode failed for slot {slot}") from handoff.error
if handoff.event is not None:
torch.cuda.current_stream(self._device).wait_event(handoff.event)
with self._lock:
self._handoffs.pop(slot, None)
# ------------------------------------------------------------------
# Decode thread
# ------------------------------------------------------------------
def _run(self) -> None:
try:
current_omni_platform.set_device(self._device)
self._run_jobs()
except Exception as error:
logger.exception("MOSS codec first-chunk worker failed")
self._worker_error = error
finally:
with self._lock:
self._closed = True
for handoff in self._handoffs.values():
if not handoff.done.is_set():
handoff.error = self._worker_error or RuntimeError("Codec first-chunk worker stopped")
handoff.done.set()
# Accepted, unprocessed routes must not be left waiting for PCM.
while True:
try:
job = self._jobs.get_nowait()
except queue.Empty:
break
if job is not None:
self._fail_delivery(job)
@staticmethod
def _fail_delivery(job: _Job) -> None:
try:
job.delivery.fail([job.request_id])
except Exception:
logger.exception("Codec first-chunk error delivery failed for %s", job.request_id)
def _run_jobs(self) -> None:
while True:
job = self._jobs.get()
if job is None:
return
jobs = [job]
while len(jobs) < self._max_batch:
try:
extra = self._jobs.get_nowait()
except queue.Empty:
break
if extra is None:
self._jobs.put(None)
break
jobs.append(extra)
try:
start = time.perf_counter()
self._decode(jobs)
self._decode_s += time.perf_counter() - start
self._batches += 1
before, self._emitted = self._emitted, self._emitted + len(jobs)
if self._emitted // 1000 != before // 1000:
logger.info(
"MOSS codec first-chunk fast path: %d chunks in %d batches, %.2f ms/batch",
self._emitted,
self._batches,
1e3 * self._decode_s / self._batches,
)
except Exception as error:
logger.exception("MOSS codec first-chunk fast decode failed for %s", [j.request_id for j in jobs])
# A partial graph replay may have advanced the slot. Surface
# the failure instead of re-decoding and advancing it twice.
for item in jobs:
item.handoff.error = error
item.handoff.done.set()
self._fail_delivery(item)
raise
@torch.no_grad()
def _decode(self, jobs: list[_Job]) -> None:
n = len(jobs)
for row, item in enumerate(jobs):
self._host_codes[:, row].copy_(item.codes)
self._host_slots[row] = item.slot
stream = self._stream
with torch.cuda.stream(stream):
for item in jobs:
self._session.order_after_reset(item.slot, stream)
codes = self._host_codes[:, :n].to(self._device, non_blocking=True).clamp_(0, self._codebook_size - 1)
slots = self._host_slots[:n].to(self._device, non_blocking=True)
result = self._wrapper.decode(codes, slots)
if result is None:
raise RuntimeError(f"no first-chunk codec graph for B={n}, T={self.frames}")
audio = result[0][:n, ..., : self._samples].float()
state_written = torch.cuda.Event()
state_written.record(stream)
self._inflight = state_written
self._host_audio[:n].view_as(audio).copy_(audio, non_blocking=True)
self._copied.record(stream)
for item in jobs:
item.handoff.event = state_written
self._copied.synchronize()
for row, item in enumerate(jobs):
wav = self._host_audio[row].clone()
if self._n_channels == 1:
wav = wav.reshape(-1)
try:
item.delivery([item.request_id], [wav], self._sr)
except Exception:
logger.exception("MOSS codec first-chunk output failed for %s", item.request_id)
with self._lock:
self._unsent_audio[item.request_key] = wav
finally:
item.handoff.done.set()
@@ -0,0 +1,447 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""MOSS Local request-slot state, independent of each step's batch order.
State writes, gathers and MTP replay run on the runner stream. Output gathers
own their storage; the runner retains its normal asynchronous output snapshot.
No request dictionary holds a view into a mutable slot or CUDA graph buffer.
"""
import numpy as np
import torch
from vllm.config import CUDAGraphMode
from vllm.forward_context import set_forward_context
from vllm.logger import init_logger
from vllm.utils.torch_utils import async_tensor_h2d
from vllm.v1.worker.gpu.input_batch import get_num_sampled_and_rejected
from vllm.v1.worker.gpu.sample.output import SamplerOutput
from vllm.v1.worker.gpu.sample.sampler import Sampler
from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
logger = init_logger(__name__)
def _metadata_to_device(data: np.ndarray, device: torch.device) -> torch.Tensor:
# CPU state tests need no CUDA allocator. The serving CUDA path retains
# vLLM's asynchronous pinned upload and its ownership/lifetime handling.
if device.type == "cpu":
return torch.from_numpy(data)
return async_tensor_h2d(data, device=device)
class _CodeRowsSnapshot(PackedOutputSnapshot):
"""Owned per-request code rows backed by one gathered device tensor.
The generic snapshot flattens every per-request leaf, concatenates them
into a slab and slices each back, then slices each again after the D2H.
Here the gathered ``[decode_rows, n_vq]`` tensor is already owned, so the
copy is a single D2H and the per-request views come from one ``split``.
"""
def __init__(self, codes: torch.Tensor, decode_rows: list[int], num_reqs: int) -> None:
dict.__init__(self, {"codes": {"audio": self._rows(codes, decode_rows, num_reqs)}})
self._codes = codes
self._decode_rows = decode_rows
self._num_reqs = num_reqs
self.producer_event = None
@staticmethod
def _rows(codes: torch.Tensor, decode_rows: list[int], num_reqs: int) -> list[torch.Tensor]:
outputs = [codes.new_empty((0, codes.shape[-1]))] * num_reqs
for batch_row, row in zip(decode_rows, codes.split(1), strict=True):
outputs[batch_row] = row
return outputs
def copy_to_cpu(self, copy_tensor):
cpu = copy_tensor(self._codes)
return {"codes": {"audio": self._rows(cpu, self._decode_rows, self._num_reqs)}}
class MossLocalModelState(OmniModelState):
def __init__(self, vllm_config, model, encoder_cache, device):
super().__init__(vllm_config, model, encoder_cache, device)
self._init_slot_buffers(self.scheduler_config.max_num_seqs, model.hidden_size, device, self.dtype)
self._batch_prefill = bool(getattr(model.config, "mrv2_batch_prefill", False))
self._direct_tokens = bool(getattr(model.config, "mrv2_direct_tokens", False))
self._local_eager_mtp = bool(getattr(model.config, "mrv2_eager_mtp", False))
logger.info(
"MOSS Local MRV2 batch prefill=%s, determined tokens=%s, eager MTP=%s",
self._batch_prefill,
self._direct_tokens,
self._local_eager_mtp,
)
logger.info("MOSS Local MRV2 GPU slot state enabled: capacity=%d", self.scheduler_config.max_num_seqs)
def _init_slot_buffers(self, capacity, hidden_size, device, dtype):
self._hidden_pool = torch.zeros((capacity, hidden_size), dtype=dtype, device=device)
self._codes_pool = torch.full(
(capacity, self.model.n_vq), self.model.audio_pad_token_id, dtype=torch.long, device=device
)
# Execution eligibility mirrors audio_state.is_stopping. Binary MTP
# continuation is a per-step output, not a sticky request-state update.
self._active_pool = torch.zeros(capacity, dtype=torch.bool, device=device)
self._active_host = [False] * capacity
self._decode_rows = []
self._prefill_staging = []
self._batch_prefill = False
self._direct_tokens = False
# Eager MTP: a frame is sampled right after the forward that produced
# its hidden state, one step earlier than the canonical preprocess
# path. Its input-embedding contribution (audio embedding times emit
# mask) is added to the next step's text embedding; ``_keep_pool``
# chains emission so a stopped stream cannot emit again.
self._local_eager_mtp = False
self._eager_emb_pool = torch.zeros((capacity, hidden_size), dtype=dtype, device=device)
self._keep_pool = torch.zeros(capacity, dtype=torch.bool, device=device)
self._completing_rows = []
self._local_eager_rows = []
# A slot takes the eager path only after its final prefill chunk ran
# an eager MTP in this state; others keep the canonical path, so a
# reused slot never reads a previous request's frame state.
self._eager_host = [False] * capacity
self._mtp_dispatcher = None
def sample_determined_tokens(self, batch, sampler):
"""Skip full-vocabulary sampling only for unconstrained Local tokens.
The audio/binary MTP sampler has already made the random decision. The
outer text distribution has exactly one finite logit. Keep the original
path for processors that can mask it or request distribution metadata.
"""
if not self._direct_tokens or type(sampler) is not Sampler:
return None
rows = batch.idx_mapping_np
if (
sampler.compute_nans
or sampler.return_sampling_mask
or sampler.trace_replay_state is not None
or sampler.get_logprobs_dims(rows) is not None
or np.any(sampler.logit_bias_state.use_logit_bias[rows])
or np.any(sampler.penalties_state.use_penalty[rows])
or np.any(sampler.bad_words_state.num_bad_words.np[rows])
or (
sampler.thinking_budget_state.enabled
and np.any(sampler.thinking_budget_state.use_thinking_budget[rows])
)
):
return None
keep = self.model._batch_should_continue
if keep is None or keep.numel() != batch.num_reqs:
return None
# Owned storage: subsequent forwards can replace the continue mask
# while these token IDs are being copied or delivered asynchronously.
tokens = torch.where(keep, self.model.audio_assistant_slot_token_id, self.model.im_end_token_id)
num_sampled, num_rejected = get_num_sampled_and_rejected(
batch.seq_lens.new_ones(batch.num_reqs),
batch.seq_lens,
batch.cu_num_logits,
batch.idx_mapping,
sampler.req_states.prefill_len.gpu,
)
return SamplerOutput(tokens.reshape(-1, 1), None, None, num_sampled, num_rejected)
def _apply_reference_batch(self, embeds, references):
"""One owned pinned upload for reference codes and their token positions.
Host storage may be reused only after its H2D event completes. Device
storage is consumed and overwritten in runner-stream order. If all
cached slots are busy, allocate a transient snapshot instead of waiting.
"""
if not references:
return
count = sum(codes.shape[0] for _, codes in references)
capacity = max(count, self._static_inputs_embeds.shape[0])
staging = None
for item in self._prefill_staging:
if item[0].shape[0] >= count and (item[2] is None or item[2].query()):
staging = item
break
if staging is None:
host = torch.empty((capacity, self.model.n_vq + 1), dtype=torch.long, pin_memory=embeds.is_cuda)
device = torch.empty_like(host, device=embeds.device) if embeds.is_cuda else host
staging = [host, device, torch.cuda.Event() if embeds.is_cuda else None]
if len(self._prefill_staging) < 3:
self._prefill_staging.append(staging)
host, device, event = staging
offset = 0
for start, codes in references:
end = offset + codes.shape[0]
host[offset:end, 0].numpy()[:] = np.arange(start, start + codes.shape[0])
host[offset:end, 1:].copy_(codes)
offset = end
if embeds.is_cuda:
device[:count].copy_(host[:count], non_blocking=True)
event.record(torch.cuda.current_stream(embeds.device))
packed = device[:count]
positions = packed[:, 0]
audio = self.model._audio_embed(packed[:, 1:])
embeds.index_copy_(0, positions, embeds.index_select(0, positions) + audio)
def add_request(self, req_index, new_req_data):
super().add_request(req_index, new_req_data)
self._hidden_pool[req_index].zero_()
self._codes_pool[req_index].fill_(self.model.audio_pad_token_id)
self._active_pool[req_index].fill_(True)
self._active_host[req_index] = True
self._eager_host[req_index] = False
self._keep_pool[req_index].fill_(False)
self._eager_emb_pool[req_index].zero_()
def remove_request(self, req_index_or_id):
# Freeing is CPU bookkeeping. Reinitialization happens on admission,
# in stream order, so pending output snapshots remain independent.
super().remove_request(req_index_or_id)
def _select_rows(self, tensor, rows):
if rows == list(range(len(rows))):
return tensor[: len(rows)]
indices = _metadata_to_device(np.asarray(rows, dtype=np.int64), tensor.device)
return tensor.index_select(0, indices)
def run_preprocess(self, input_batch, model_inputs, req_states=None, mtp_batch_descriptor_dispatcher=None):
input_ids = model_inputs.get("input_ids")
if input_ids is None:
input_ids = input_batch.input_ids
embeds = model_inputs.get("inputs_embeds")
if embeds is None:
embeds = self.model.embed_input_ids(input_ids[: input_batch.num_tokens])
model_inputs["inputs_embeds"] = embeds
elif self._static_inputs_embeds is not None and embeds.data_ptr() == self._static_inputs_embeds.data_ptr():
embeds[: input_batch.num_tokens].copy_(self.model.embed_input_ids(input_ids[: input_batch.num_tokens]))
decode_rows = []
completing_rows = []
references = []
for row in range(input_batch.num_reqs):
slot = int(input_batch.idx_mapping_np[row])
buf = self.intermediate_buffer.buffers[slot]
if not buf or str(buf.get("req_id", "")).startswith("_warmup_"):
continue
start = int(input_batch.query_start_loc_np[row])
count = int(input_batch.num_scheduled_tokens[row])
prompt_len = (
None if req_states is None else self._get_req_state_value(getattr(req_states, "prompt_len", None), slot)
)
computed = (
None
if req_states is None
else self._get_req_state_value(getattr(req_states, "num_computed_tokens", None), slot)
)
if computed is None:
computed = self._get_input_batch_num_computed(input_batch, slot, row)
prefill = computed < prompt_len if computed is not None and prompt_len is not None else count > 1
if prefill and computed is not None and prompt_len is not None and computed + count >= prompt_len:
completing_rows.append(row)
if count == 1 and not prefill and isinstance(buf.get("audio_state"), dict):
active = not bool(buf["audio_state"].get("is_stopping"))
if active != self._active_host[slot]:
self._active_pool[slot].fill_(active)
self._active_host[slot] = active
decode_rows.append(row)
continue
if self._batch_prefill:
ref = (buf.get("codes", {}) or {}).get("ref")
ref_offset = int(buf.get("ref_offset", 0))
# Device-side references keep the canonical path; do not add
# an implicit D2H just to place them in a host staging buffer.
if not isinstance(ref, torch.Tensor) or ref.device.type == "cpu":
if isinstance(ref, torch.Tensor) and ref.numel():
if ref.dim() == 1 and ref.numel() % self.model.n_vq == 0:
ref = ref.view(-1, self.model.n_vq)
if ref.dim() == 2:
chunk = ref[ref_offset : ref_offset + count]
if chunk.numel() and chunk.shape[0] == count:
references.append((start, chunk))
# Text embeddings have already been computed for all input
# tokens. Match the canonical prefill state transition.
self.intermediate_buffer.update(
slot,
{
"audio_state": {"is_stopping": False},
"ref_offset": ref_offset + count,
},
self.model.gpu_resident_buffer_keys,
)
if not self._active_host[slot]:
self._active_pool[slot].fill_(True)
self._active_host[slot] = True
continue
# Reference conditioning and chunked-prefill coordinates retain
# the model's canonical scalar implementation.
info = dict(buf)
info["_omni_is_prefill"] = prefill
if prompt_len is not None:
info["_omni_prompt_len"] = prompt_len
if computed is not None:
info["_omni_num_computed_tokens"] = computed
ids = input_ids[start : start + count]
previous = embeds[start : start + count]
new_ids, new_embeds, updates = self.model.preprocess(ids, previous, **info)
if "mtp_inputs" in updates:
raise RuntimeError("MOSS slot state requires an admitted prefill before decode")
if self._preprocess_result_needs_writeback(ids, new_ids):
ids.copy_(new_ids)
if self._preprocess_result_needs_writeback(previous, new_embeds):
previous.copy_(new_embeds)
self.intermediate_buffer.update(slot, updates, self.model.gpu_resident_buffer_keys)
active = not bool(updates.get("audio_state", {}).get("is_stopping", False))
if active != self._active_host[slot]:
self._active_pool[slot].fill_(active)
self._active_host[slot] = active
self._apply_reference_batch(embeds, references)
self._decode_rows = decode_rows
if self._local_eager_mtp:
self._completing_rows = completing_rows
self._mtp_dispatcher = mtp_batch_descriptor_dispatcher
eager_rows = [row for row in decode_rows if self._eager_host[int(input_batch.idx_mapping_np[row])]]
canonical_rows = [row for row in decode_rows if not self._eager_host[int(input_batch.idx_mapping_np[row])]]
self._local_eager_rows = eager_rows
if eager_rows:
# Their frame was sampled after the previous forward.
slots = self._select_rows(input_batch.idx_mapping, eager_rows)
offsets = self._mtp_offsets[: len(eager_rows)]
offsets.copy_(self._select_rows(input_batch.query_start_loc, eager_rows))
embeds.index_copy_(
0, offsets, embeds.index_select(0, offsets) + self._eager_emb_pool.index_select(0, slots)
)
if canonical_rows:
self._run_slot_mtp(input_batch, input_ids, embeds, canonical_rows, mtp_batch_descriptor_dispatcher)
elif decode_rows:
self._run_slot_mtp(input_batch, input_ids, embeds, decode_rows, mtp_batch_descriptor_dispatcher)
def _run_slot_mtp(self, batch, input_ids, embeds, rows, dispatcher):
bsz = len(rows)
slots = self._select_rows(batch.idx_mapping, rows)
# Upstream query_start_loc is int32; index_copy_ requires int64.
offsets = self._mtp_offsets[:bsz]
offsets.copy_(self._select_rows(batch.query_start_loc, rows))
ids, emb = self._mtp_input_ids[:bsz], self._mtp_input_embeds[:bsz]
hidden, step = self._mtp_hidden[:bsz], self._mtp_text_step[:bsz]
ids.copy_(input_ids.index_select(0, offsets)) # MRV2 token inputs are int32.
torch.index_select(embeds, 0, offsets, out=emb)
torch.index_select(self._hidden_pool, 0, slots, out=hidden)
step[:, 0].copy_(self._active_pool.index_select(0, slots))
buffers = [self.intermediate_buffer.buffers[int(batch.idx_mapping_np[row])] for row in rows]
req_ids = [str(buf["req_id"]) for buf in buffers]
generators = [
self._get_mtp_generator(req_id, buf.get("sampling_params"), ids.device)
for req_id, buf in zip(req_ids, buffers, strict=True)
]
can_graph = self._is_mtp_graph_runner() and not any(g is not None for g in generators)
desc = dispatcher(bsz) if can_graph and dispatcher is not None else None
mode = getattr(desc, "cg_mode", CUDAGraphMode.FULL) if can_graph else CUDAGraphMode.NONE
num_tokens = int(desc.num_tokens) if desc is not None else bsz
with set_forward_context(
None, self.vllm_config, num_tokens=num_tokens, cudagraph_runtime_mode=mode, batch_descriptor=desc
):
new_emb, codes = self._call_mtp_with_sampling(
self._mtp_input_ids[:num_tokens],
self._mtp_input_embeds[:num_tokens],
self._mtp_hidden[:num_tokens],
self._mtp_text_step[:num_tokens],
buffers=buffers,
req_ids=req_ids,
generators=generators,
)
embeds.index_copy_(0, offsets, new_emb[:bsz])
self._codes_pool.index_copy_(0, slots, codes[:bsz])
def _run_local_eager_mtp(self, batch, rows, completing):
"""Sample the next frame from this forward's hidden state.
Same MTP inputs as the canonical next-step preprocess (hidden state
and active flag); the input embedding is zero, so the returned
embedding is exactly the audio contribution the next step adds to its
text embedding. A decode row emits only if its previous frame did.
"""
bsz = len(rows)
slots = self._select_rows(batch.idx_mapping, rows)
ids, emb = self._mtp_input_ids[:bsz], self._mtp_input_embeds[:bsz]
hidden, step = self._mtp_hidden[:bsz], self._mtp_text_step[:bsz]
ids.zero_() # the MTP does not read token ids
emb.zero_()
torch.index_select(self._hidden_pool, 0, slots, out=hidden)
gate = self._keep_pool.index_select(0, slots)
if completing:
first = _metadata_to_device(
np.asarray([row in completing for row in rows], dtype=np.bool_),
gate.device,
)
gate = gate | first
step[:, 0].copy_(self._active_pool.index_select(0, slots) & gate)
buffers = [self.intermediate_buffer.buffers[int(batch.idx_mapping_np[row])] for row in rows]
req_ids = [str(buf["req_id"]) for buf in buffers]
generators = [
self._get_mtp_generator(req_id, buf.get("sampling_params"), ids.device)
for req_id, buf in zip(req_ids, buffers, strict=True)
]
dispatcher = self._mtp_dispatcher
can_graph = self._is_mtp_graph_runner() and not any(g is not None for g in generators)
desc = dispatcher(bsz) if can_graph and dispatcher is not None else None
mode = getattr(desc, "cg_mode", CUDAGraphMode.FULL) if can_graph else CUDAGraphMode.NONE
num_tokens = int(desc.num_tokens) if desc is not None else bsz
with set_forward_context(
None, self.vllm_config, num_tokens=num_tokens, cudagraph_runtime_mode=mode, batch_descriptor=desc
):
contribution, codes = self._call_mtp_with_sampling(
self._mtp_input_ids[:num_tokens],
self._mtp_input_embeds[:num_tokens],
self._mtp_hidden[:num_tokens],
self._mtp_text_step[:num_tokens],
buffers=buffers,
req_ids=req_ids,
generators=generators,
)
codes = codes[:bsz]
self._eager_emb_pool.index_copy_(0, slots, contribution[:bsz])
self._codes_pool.index_copy_(0, slots, codes)
self._keep_pool.index_copy_(0, slots, codes.ne(self.model.audio_pad_token_id).any(dim=-1))
def run_postprocess(self, hidden_states, input_batch):
# query_start_loc is already device-resident and authoritative for
# mixed/chunked-prefill batches. index_copy snapshots graph outputs.
last = input_batch.query_start_loc[1 : input_batch.num_reqs + 1] - 1
self._hidden_pool.index_copy_(
0, input_batch.idx_mapping[: input_batch.num_reqs], hidden_states.index_select(0, last)
)
if self._local_eager_mtp:
completing = self._completing_rows
rows = sorted(self._local_eager_rows + completing)
# Canonical decode rows emit their preprocess frame; eager rows
# and completed prefills emit the frame sampled below.
self._decode_rows = sorted(set(self._decode_rows) | set(completing))
if rows:
self._run_local_eager_mtp(input_batch, rows, set(completing))
for row in completing:
self._eager_host[int(input_batch.idx_mapping_np[row])] = True
def postprocess_model_output(self, model_output, input_batch, req_states):
# Local backbone returns a tensor; keep the standard runner's output
# snapshot/materializer contract and omit hidden payload transport.
if not isinstance(model_output, torch.Tensor):
raise TypeError("MOSS Local slot state requires a tensor backbone output")
model = self.model
model._batch_state = None
model._batch_state_spans = None
model._batch_should_continue = torch.ones(input_batch.num_reqs, device=model_output.device, dtype=torch.bool)
if not self._decode_rows:
return model_output, {}
# This gather is an owned snapshot. Later slot writes cannot change
# data already handed to the runner, even before its D2H completes.
slots = self._select_rows(input_batch.idx_mapping, self._decode_rows)
codes = self._codes_pool.index_select(0, slots)
keep = codes.ne(model.audio_pad_token_id).any(dim=-1)
if self._decode_rows == list(range(input_batch.num_reqs)):
model._batch_should_continue = keep
else:
rows = _metadata_to_device(
np.asarray(self._decode_rows, dtype=np.int64),
codes.device,
)
model._batch_should_continue.index_copy_(0, rows, keep)
return model_output, _CodeRowsSnapshot(codes, list(self._decode_rows), input_batch.num_reqs)
@@ -0,0 +1,100 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Single-query attention for the frame-local MOSS depth transformer.
The caller has already written this token's K/V and provides only its valid
prefix. There is no persistent state here. BF16 probability rounding matches
the FlashAttention P/V multiply, but reduction order is not bit-identical.
"""
import torch
from vllm.triton_utils import tl, triton
@triton.jit
def _short_attention(
q_ptr,
k_ptr,
v_ptr,
out_ptr,
qs0: tl.constexpr,
qs1: tl.constexpr,
ks0: tl.constexpr,
ks1: tl.constexpr,
ks2: tl.constexpr,
vs0: tl.constexpr,
vs1: tl.constexpr,
vs2: tl.constexpr,
heads: tl.constexpr,
dim: tl.constexpr,
tokens: tl.constexpr,
block_d: tl.constexpr,
block_t: tl.constexpr,
):
row = tl.program_id(0)
batch, head = row // heads, row % heads
d, t = tl.arange(0, block_d), tl.arange(0, block_t)
query = tl.load(q_ptr + batch * qs0 + head * qs1 + d, d < dim, 0).to(tl.float32)
keys = tl.load(
k_ptr + batch * ks0 + head * ks1 + t[:, None] * ks2 + d[None, :],
(t[:, None] < tokens) & (d[None, :] < dim),
0,
).to(tl.float32)
logits = tl.sum(keys * query[None, :], axis=1) * (dim**-0.5 * 1.4426950408889634)
logits = tl.where(t < tokens, logits, float("-inf"))
probs = tl.exp2(logits - tl.max(logits, axis=0))
denominator = tl.sum(probs, axis=0)
values = tl.load(
v_ptr + batch * vs0 + head * vs1 + t[:, None] * vs2 + d[None, :],
(t[:, None] < tokens) & (d[None, :] < dim),
0,
)
probs = probs.to(values.dtype).to(tl.float32)
output = tl.sum(values.to(tl.float32) * probs[:, None], axis=0) / denominator
tl.store(out_ptr + row * dim + d, output, d < dim)
@torch.library.custom_op("vllm_omni::moss_local_short_attention", mutates_args=())
def local_short_attention(query: torch.Tensor, key: torch.Tensor, value: torch.Tensor) -> torch.Tensor:
batch, heads, seq_len, dim = query.shape
if not (
query.is_cuda
and key.device == query.device
and value.device == query.device
and query.dtype == key.dtype == value.dtype == torch.bfloat16
and seq_len == 1
and 0 < dim <= 128
and key.shape == value.shape
and key.shape[:2] == query.shape[:2]
and key.shape[-1] == dim
and 0 < key.shape[2] <= 16
and query.stride(-1) == key.stride(-1) == value.stride(-1) == 1
):
raise ValueError("MOSS short attention requires CUDA BF16 single-query inputs and 1..16 valid keys")
output = query.new_empty((batch, heads, 1, dim))
_short_attention[(batch * heads,)](
query,
key,
value,
output,
query.stride(0),
query.stride(1),
key.stride(0),
key.stride(1),
key.stride(2),
value.stride(0),
value.stride(1),
value.stride(2),
heads,
dim,
key.shape[2],
triton.next_power_of_2(dim),
triton.next_power_of_2(key.shape[2]),
num_warps=1,
)
return output
@local_short_attention.register_fake
def _fake_local_short_attention(query: torch.Tensor, key: torch.Tensor, value: torch.Tensor) -> torch.Tensor:
return query.new_empty(query.shape)
@@ -8,7 +8,10 @@
from __future__ import annotations
import threading
from collections.abc import Iterable
from contextlib import nullcontext
from functools import partial
from typing import Any
import torch
@@ -32,11 +35,16 @@ from vllm_omni.model_executor.models.moss_tts.configuration_moss_audio_tokenizer
from vllm_omni.model_executor.models.moss_tts.cuda_graph_streaming_decoder_wrapper import (
CUDAGraphStreamingDecoderWrapper,
)
from vllm_omni.model_executor.models.moss_tts.first_chunk_fast_path import (
MossFirstChunkFastPath,
)
from vllm_omni.model_executor.models.moss_tts.moss_codec_cudagraph import (
MossTTSCUDAGraphCodecWrapper,
)
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot, pack_output_snapshot
from vllm_omni.model_executor.stage_input_processors.chunk_size_utils import parse_chunk_ramp
from vllm_omni.worker_v2.first_audio_sender import FirstAudioSink
logger = init_logger(__name__)
@@ -53,18 +61,46 @@ class _MossCodecStreamSession:
vllm_config: VllmConfig,
graph_batch_sizes: list[int] | None = None,
graph_frame_sizes: list[int] | None = None,
gpu_output: bool = False,
chunk_frames: int = 0,
ring_headroom: bool = False,
fast_batch_sizes: list[int] | None = None,
fast_frames: int = 0,
) -> None:
self._codec = codec
self._state_capacity = int(state_capacity)
self._chunk_frames = int(chunk_frames)
self._ring_headroom = bool(ring_headroom)
self._n_vq = int(n_vq)
self._device = next(codec.parameters()).device
self._gpu_output = gpu_output and self._device.type == "cuda"
self._free_stream_slots = list(reversed(range(self._state_capacity)))
self._leased_slots: set[int] = set()
# The first-chunk fast path leases slots from its decode thread.
self._slot_lock = threading.Lock()
self._slot_reset_events: dict[int, torch.cuda.Event] = {}
self._closed = False
self._metadata_index = 0
self._metadata_ring = []
if self._gpu_output:
# The metadata DMA is tiny, but a short ring forces the producer
# CPU thread to synchronize with the codec stream while it waits
# for a host slab to become reusable. Keep enough entries for a
# scheduler burst so staging remains enqueue-only in practice.
for _ in range(64):
host = torch.empty(2 * self._state_capacity, dtype=torch.long, pin_memory=True)
gpu = torch.empty_like(host, device=self._device)
self._metadata_ring.append((host, host.numpy(), gpu, torch.cuda.Event(), False))
self._cudagraph_wrapper: CUDAGraphStreamingDecoderWrapper | None = None
self._fast_wrapper: CUDAGraphStreamingDecoderWrapper | None = None
batch_sizes = sorted({int(size) for size in (graph_batch_sizes or []) if 0 < int(size) <= self._state_capacity})
frame_sizes = sorted({int(size) for size in (graph_frame_sizes or []) if int(size) > 0})
scratch_capacity = max(batch_sizes, default=0) if self._device.type in ("cuda", "npu") else 0
fast_batch_sizes = sorted({int(size) for size in (fast_batch_sizes or []) if 0 < int(size)})
if not (fast_frames > 0 and fast_batch_sizes and batch_sizes and frame_sizes and self._device.type == "cuda"):
fast_batch_sizes = []
fast_scratch_base = self._state_capacity + scratch_capacity
scratch_capacity += max(fast_batch_sizes, default=0)
self._total_state_capacity = self._state_capacity + scratch_capacity
self._state_slot_ids = torch.arange(
self._total_state_capacity,
@@ -76,7 +112,14 @@ class _MossCodecStreamSession:
if not callable(initialize_state_pool):
raise RuntimeError("The streaming codec does not implement a decoder state pool.")
with torch.no_grad():
initialize_state_pool(self._state_capacity, scratch_capacity)
if self._ring_headroom:
if self._chunk_frames <= 0:
raise ValueError("codec_ring_headroom requires a positive codec chunk frame count.")
# Size every attention ring for context + one full chunk so a
# chunk write never evicts keys inside its own causal window.
initialize_state_pool(self._state_capacity, scratch_capacity, chunk_frames=self._chunk_frames)
else:
initialize_state_pool(self._state_capacity, scratch_capacity)
if batch_sizes and frame_sizes and self._device.type in ("cuda", "npu"):
if self._device.type == "npu":
from vllm_omni.platforms.npu.models.moss_tts_streaming_decode_wrapper import (
@@ -101,16 +144,34 @@ class _MossCodecStreamSession:
vllm_config=vllm_config,
)
self._cudagraph_wrapper.warmup(self._device)
if fast_batch_sizes and self._cudagraph_wrapper.is_ready:
main_compiled = self._cudagraph_wrapper._compiled_decode
self._fast_wrapper = CUDAGraphStreamingDecoderWrapper(
codec,
state_capacity=self._state_capacity,
batch_sizes=fast_batch_sizes,
frame_sizes=[int(fast_frames)],
num_quantizers=self._n_vq,
vllm_config=vllm_config,
scratch_base=fast_scratch_base,
private_pool=True,
compiled_decode=main_compiled if main_compiled is not None else codec.decode_streaming_tensors,
)
self._fast_wrapper.warmup(self._device)
if not self._fast_wrapper.is_ready:
self._fast_wrapper = None
self.reset_slots(list(range(self._state_capacity + scratch_capacity)))
if not self._cudagraph_wrapper.is_ready:
self._cudagraph_wrapper = None
def acquire(self) -> int | None:
if not self._free_stream_slots:
return None
slot = self._free_stream_slots.pop()
self._leased_slots.add(slot)
return slot
def acquire(self, *, oldest: bool = False) -> int | None:
"""Lease a slot; ``oldest`` takes the one whose reset was queued first."""
with self._slot_lock:
if not self._free_stream_slots:
return None
slot = self._free_stream_slots.pop(0 if oldest else -1)
self._leased_slots.add(slot)
return slot
def release(self, slot: int, *, state_already_reset: bool = False) -> None:
if self._closed:
@@ -119,8 +180,23 @@ class _MossCodecStreamSession:
return
if not state_already_reset:
self.reset_slots([slot])
self._leased_slots.remove(slot)
self._free_stream_slots.append(slot)
reset_event = None
if self._fast_wrapper is not None:
# The fast path decodes on another stream; it must not read this
# slot before the reset queued on the current stream has run.
reset_event = torch.cuda.Event()
reset_event.record(torch.cuda.current_stream(self._device))
with self._slot_lock:
if reset_event is not None:
self._slot_reset_events[slot] = reset_event
self._leased_slots.remove(slot)
self._free_stream_slots.append(slot)
def order_after_reset(self, slot: int, stream: torch.cuda.Stream) -> None:
with self._slot_lock:
event = self._slot_reset_events.pop(slot, None)
if event is not None and not event.query():
stream.wait_event(event)
def _reset_slot_ids(self, slot_ids: torch.Tensor) -> None:
reset_streaming_slots = getattr(self._codec, "reset_decoder_state_slots", None)
@@ -139,9 +215,29 @@ class _MossCodecStreamSession:
raise ValueError(f"Decoder state slots must be contiguous, got {slots}")
self._reset_slot_ids(self._state_slot_ids[start : start + len(slots)])
def _stage_slot_ids(self, slots: list[int], terminal_slots: set[int]) -> tuple[torch.Tensor, torch.Tensor]:
# Host slabs cannot be overwritten until their DMA completes. Device
# slabs are reused in producer-stream order, after all previous reads.
index = self._metadata_index
host, view, gpu, event, recorded = self._metadata_ring[index]
if recorded and not event.query():
event.synchronize()
terminal_ids = [slot for slot in slots if slot in terminal_slots]
n, end = len(slots), len(slots) + len(terminal_ids)
view[:n] = slots
view[n:end] = terminal_ids
gpu[:end].copy_(host[:end], non_blocking=True)
event.record(torch.cuda.current_stream(self._device))
self._metadata_ring[index] = (host, view, gpu, event, True)
self._metadata_index = (index + 1) % len(self._metadata_ring)
return gpu[:n], gpu[n:end]
def close(self) -> None:
if self._closed:
return
for _, _, _, event, recorded in self._metadata_ring:
if recorded:
event.synchronize()
close_state_pool = getattr(self._codec, "close_decoder_state_pool", None)
with torch.no_grad():
if callable(close_state_pool):
@@ -154,6 +250,7 @@ class _MossCodecStreamSession:
slot_codes: dict[int, torch.Tensor],
*,
terminal_slots: set[int] | None = None,
output_buffers: dict[int, torch.Tensor] | None = None,
) -> dict[int, torch.Tensor]:
if not slot_codes:
return {}
@@ -178,17 +275,22 @@ class _MossCodecStreamSession:
],
dim=1,
)
state_slot_ids = torch.tensor(slots, device=self._device, dtype=torch.long)
staged_terminal_ids = None
if self._metadata_ring:
state_slot_ids, staged_terminal_ids = self._stage_slot_ids(slots, terminal_slots)
else:
state_slot_ids = torch.tensor(slots, device=self._device, dtype=torch.long)
graph_output: tuple[torch.Tensor, torch.Tensor, int] | None = None
if self._cudagraph_wrapper is not None:
# A terminal tail can safely pad T to a larger graph: decoder
# attention is causal, audio_lengths trims the padding, and every
# affected state slot is reset immediately after this step.
# An all-terminal group may borrow the next larger frame graph.
# The decoder is causal, so cropping the padded tail on the time
# axis recovers the exact-T waveform up to attention-ring
# eviction (see codec_ring_headroom); the state is reset below.
graph_output = self._cudagraph_wrapper.decode(
codes_step,
state_slot_ids,
allow_frame_padding=len(terminal_slots) == len(slots),
allow_frame_padding=bool(terminal_slots) and len(terminal_slots) == len(slots),
)
used_cudagraph = graph_output is not None
@@ -214,11 +316,69 @@ class _MossCodecStreamSession:
audio_tensor = result.audio
if terminal_slots:
terminal_rows = [row for row, slot in enumerate(slots) if slot in terminal_slots]
terminal_slot_ids = state_slot_ids if len(terminal_rows) == len(slots) else state_slot_ids[terminal_rows]
if staged_terminal_ids is not None:
terminal_slot_ids = staged_terminal_ids
else:
terminal_rows = [row for row, slot in enumerate(slots) if slot in terminal_slots]
terminal_slot_ids = (
state_slot_ids if len(terminal_rows) == len(slots) else state_slot_ids[terminal_rows]
)
self._reset_slot_ids(terminal_slot_ids)
audio = audio_tensor.detach().to("cpu", torch.float32)
if self._gpu_output and output_buffers is not None:
# Write directly into the caller-owned per-forward slab. This
# preserves graph output before the next replay without creating
# a temporary waveform tensor that would be packed again later.
out: dict[int, torch.Tensor] = {}
row = 0
while row < len(slots):
destination = output_buffers.get(slots[row])
if destination is None:
row += 1
continue
end = row + 1
if end < len(slots) and destination.is_contiguous():
storage = destination.untyped_storage().data_ptr()
offset = destination.storage_offset()
row_elements = destination.numel()
while end < len(slots):
following = output_buffers.get(slots[end])
if not (
following is not None
and following.shape == destination.shape
and following.dtype == destination.dtype
and following.device == destination.device
and following.is_contiguous()
and following.untyped_storage().data_ptr() == storage
and following.storage_offset() == offset + (end - row) * row_elements
):
break
end += 1
if end > row + 1:
# A mixed-frame forward can leave gaps between equal-sized
# rows in its slab. Copy each adjacent run as one batch,
# without overwriting the intervening requests' output.
destination = destination.as_strided(
(end - row, *destination.shape), (destination.numel(), *destination.stride())
)
source = audio_tensor[row:end]
else:
source = audio_tensor[row]
# Crop time before reshaping: flattening first mixes stereo
# channels whenever a row is shorter than the decode batch.
destination.copy_(source[..., : destination.shape[-1]].view_as(destination))
out.update((slot, output_buffers[slot]) for slot in slots[row:end])
row = end
return out
if self._gpu_output:
# A graph's static output is overwritten on every replay, including
# multiple replays within one forward. Snapshot on the producer
# stream before returning; record_stream alone cannot prevent this
# overwrite. MRV2 owns the subsequent pinned, asynchronous D2H.
audio = audio_tensor.detach().to(dtype=torch.float32, copy=True)
else:
audio = audio_tensor.detach().to("cpu", torch.float32)
out: dict[int, torch.Tensor] = {}
for row, slot in enumerate(slots):
audio_length = int(slot_codes[slot].shape[1]) * self._samples_per_frame
@@ -270,6 +430,7 @@ class MossTTSCodecDecoder(nn.Module):
requires_exact_input_shape: bool = True
_OUTPUT_SAMPLE_RATE: int = 24_000
_first_chunk_fast_path: MossFirstChunkFastPath | None = None
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
super().__init__()
@@ -292,20 +453,38 @@ class MossTTSCodecDecoder(nn.Module):
self._n_channels: int = 1
self._sr_tensor = torch.tensor(self._OUTPUT_SAMPLE_RATE, dtype=torch.int32)
self._stream_session: _MossCodecStreamSession | None = None
self._codec_stream: torch.cuda.Stream | None = None
scheduler_cfg = getattr(self.vllm_config, "scheduler_config", None)
self._stream_state_capacity = max(1, int(getattr(scheduler_cfg, "max_num_seqs", 1) or 1))
self._initial_stream_chunk_frames: int = self._connector_int("initial_codec_chunk_frames", 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
# Opt-in: size codec attention rings as context + one chunk so chunked
# streaming reproduces whole-sequence decoding (costs ring memory).
self._stream_ring_headroom: bool = bool(self._connector_int("codec_ring_headroom", default=0))
self._stream_req_slots: dict[str, int] = {}
# Opt-in: decode each stream's first chunk from the receive thread.
self._first_chunk_fast = bool(self._connector_int("codec_first_chunk_fast_path", default=0))
self._first_chunk_fast_path: MossFirstChunkFastPath | None = None
self._async_chunk = bool(getattr(self.vllm_config.model_config, "async_chunk", False))
self._gpu_stream_output = (
self._async_chunk
and bool(getattr(vllm_config.model_config, "use_v2_model_runner", False))
and getattr(cfg, "model_type", None) == "moss_tts_local"
# Diagnostic switch: 0 copies each decode group's batch to the host synchronously.
and bool(self._connector_int("codec_gpu_stream_output", default=1))
)
self._streaming_graph_batch_sizes = self._streaming_graph_batch_sizes_from_compilation_config()
# codec_chunk_ramp ladder sizes join the streaming graph frame-size
# set so every ladder (B, T) combination is captured during warmup.
ramp = parse_chunk_ramp(self._connector_extra(), steady=self._stream_chunk_frames)
if ramp:
# The sender gives the ramp precedence over initial chunk size.
# The dedicated first-chunk graph must use that same first shape.
self._initial_stream_chunk_frames = ramp[0]
self._stream_max_step_frames = max(
self._stream_max_step_frames, self._initial_stream_chunk_frames, *(ramp or [])
)
self._streaming_graph_frame_sizes = _resolve_streaming_graph_frame_sizes(
self._initial_stream_chunk_frames,
self._stream_chunk_frames,
self._connector_extra_cfg(),
self._initial_stream_chunk_frames, self._stream_chunk_frames, self._connector_extra()
)
# ------------------------------------------------------------------
@@ -337,6 +516,41 @@ class MossTTSCodecDecoder(nn.Module):
inputs_embeds: torch.Tensor | None = None,
runtime_additional_information: list[dict[str, Any]] | None = None,
**kwargs: Any,
) -> OmniOutput:
if self._gpu_stream_output and input_ids is not None and input_ids.is_cuda:
if self._codec_stream is None:
self._codec_stream = torch.cuda.Stream(device=input_ids.device)
main_stream = torch.cuda.current_stream(input_ids.device)
# Runner input storage is reused by the next batch on main_stream.
# Own it before allowing that preparation to overlap codec decode.
owned_input_ids = input_ids.clone()
with torch.cuda.stream(self._codec_stream):
self._codec_stream.wait_stream(main_stream)
owned_input_ids.record_stream(self._codec_stream)
output = self._forward(
owned_input_ids,
positions,
intermediate_tensors,
inputs_embeds,
runtime_additional_information,
**kwargs,
)
if isinstance(output.multimodal_outputs, PackedOutputSnapshot):
output.multimodal_outputs.record_producer_event(self._codec_stream)
return output
return self._forward(
input_ids, positions, intermediate_tensors, inputs_embeds, runtime_additional_information, **kwargs
)
@torch.no_grad()
def _forward(
self,
input_ids: torch.Tensor | None = None,
positions: torch.Tensor | None = None,
intermediate_tensors: Any = None,
inputs_embeds: torch.Tensor | None = None,
runtime_additional_information: list[dict[str, Any]] | None = None,
**kwargs: Any,
) -> OmniOutput:
"""Decode audio VQ codes to waveform.
@@ -493,13 +707,37 @@ class MossTTSCodecDecoder(nn.Module):
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():
stream_output_slab = None
stream_output_buffers: dict[int, torch.Tensor] = {}
if self._gpu_stream_output and device.type == "cuda":
total_elements = 0
for output_index, _, codes_nq_t, _ in streaming_work:
samples = int(codes_nq_t.shape[1]) * self._codec.downsample_rate
total_elements += samples * (self._n_channels if self._n_channels > 1 else 1)
if total_elements:
stream_output_slab = torch.empty(total_elements, dtype=torch.float32, device=device)
offset = 0
for output_index, _, codes_nq_t, _ in streaming_work:
samples = int(codes_nq_t.shape[1]) * self._codec.downsample_rate
elements = samples * (self._n_channels if self._n_channels > 1 else 1)
shape = (self._n_channels, samples) if self._n_channels > 1 else (samples,)
stream_output_buffers[output_index] = stream_output_slab.narrow(0, offset, elements).view(shape)
offset += elements
for i, wav in self._decode_streaming_batch(
streaming_work,
output_buffers=stream_output_buffers or None,
).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},
)
payload = {"model_outputs": audios, "sr": srs}
if self._gpu_stream_output and device.type == "cuda":
# One transfer per dtype/device preserves ragged request lengths
# without issuing one D2H per waveform. A fresh slot owns each
# output until the runner completes its copy; no reusable ring.
packed = pack_output_snapshot(payload, {}, max_buckets=0, reuse_existing_storage=True)
if packed is not None:
payload = packed
return OmniOutput(text_hidden_states=None, multimodal_outputs=payload)
@staticmethod
def _normalize_seq_token_counts(value: Any) -> list[int] | None:
@@ -546,25 +784,104 @@ class MossTTSCodecDecoder(nn.Module):
vllm_config=self.vllm_config,
graph_batch_sizes=self._streaming_graph_batch_sizes,
graph_frame_sizes=self._streaming_graph_frame_sizes,
gpu_output=self._gpu_stream_output,
chunk_frames=self._stream_max_step_frames,
ring_headroom=self._stream_ring_headroom,
fast_batch_sizes=[1, 2, 4, 8] if self._first_chunk_fast else None,
fast_frames=self._initial_stream_chunk_frames if self._first_chunk_fast else 0,
)
if self._stream_session._fast_wrapper is not None:
self._first_chunk_fast_path = MossFirstChunkFastPath(
self._stream_session,
self._stream_session._fast_wrapper,
n_vq=self._n_vq,
frames=self._initial_stream_chunk_frames,
codebook_size=int(self._codec.config.codebook_size),
samples_per_frame=int(self._codec.downsample_rate),
n_channels=self._n_channels,
sample_rate=self._sr_tensor,
device=next(self._codec.parameters()).device,
gate_main=bool(self._connector_int("codec_first_chunk_gate", default=0)),
)
logger.info(
"MOSS-TTS codec first-chunk fast path ready: B=%s T=%d",
self._stream_session._fast_wrapper.capture_sizes,
self._initial_stream_chunk_frames,
)
elif self._first_chunk_fast:
logger.warning("MOSS-TTS codec first-chunk fast path requested but unavailable; using the main path.")
if self._stream_ring_headroom:
logger.info(
"MOSS-TTS codec attention rings sized with headroom for %d-frame chunks",
self._stream_max_step_frames,
)
return self._stream_session
def bind_first_chunk_fast_path(self, sink: FirstAudioSink) -> Any:
"""Start the fast path with an output sink; returns the receive hook."""
fast = self._first_chunk_fast_path
if fast is None:
return None
fast.bind(sink)
return self._submit_first_chunk
def _submit_first_chunk(self, request_id: str, handle: Any, payload: Any) -> bool:
fast = self._first_chunk_fast_path
if fast is None or not isinstance(payload, dict):
return False
codes = payload.get("codes")
codes = codes.get("audio") if isinstance(codes, dict) else None
if codes is None:
return False
meta = payload.get("meta") if isinstance(payload.get("meta"), dict) else {}
key = meta.get("req_id")
if isinstance(key, (list, tuple)):
key = key[0] if key else None
if key is None:
key = getattr(handle, "external_req_id", None) or request_id
return fast.submit(request_id, str(key), codes, self._stream_req_slots)
def _decode_streaming_batch(
self,
items: list[tuple[int, str, torch.Tensor, bool]],
*,
output_buffers: dict[int, torch.Tensor] | None = None,
) -> dict[int, torch.Tensor]:
session = self._ensure_stream_session()
if session is None:
return {}
outputs: dict[int, torch.Tensor] = {}
unsent_first_audio: 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))
graph = session._cudagraph_wrapper
# Terminal tails join the regular padded frame bucket when every
# execution batch width has a graph, so a step stays one replay
# instead of one eager or plain-graph decode per distinct tail length.
coalesce_tails = graph is not None and max(graph.batch_sizes, default=0) >= self._stream_state_capacity
fast = self._first_chunk_fast_path
for output_index, request_id, codes_nq_t, finished in items:
slot = self._stream_req_slots.get(request_id)
if fast is not None and slot is not None:
fast.order_after(slot)
if fast.take_decoded(request_id):
unsent = fast.take_unsent_audio(request_id)
if unsent is not None:
unsent_first_audio[output_index] = unsent
# The fast path already emitted this chunk's audio and
# advanced the slot state past it.
codes_nq_t = codes_nq_t[:, fast.frames :]
if codes_nq_t.shape[1] == 0:
if unsent is not None:
outputs[output_index] = unsent
if finished:
self._finish_stream_request(request_id, session, slot)
continue
if output_buffers is not None:
# Sized for the untrimmed chunk; decode into a fresh tensor.
output_buffers.pop(output_index, None)
if slot is None:
slot = session.acquire()
if slot is None:
@@ -580,9 +897,11 @@ class MossTTSCodecDecoder(nn.Module):
slot,
codes_nq_t,
terminal=finished,
output_buffer=output_buffers.get(output_index) if output_buffers is not None else None,
)
if wav is not None:
outputs[output_index] = wav
prefix = unsent_first_audio.pop(output_index, None)
outputs[output_index] = torch.cat((prefix, wav), dim=-1) if prefix is not None else wav
if finished:
self._finish_stream_request(
request_id,
@@ -593,21 +912,33 @@ class MossTTSCodecDecoder(nn.Module):
continue
frame_count = int(codes_nq_t.shape[1])
# Only combine tails that already used the same padded graph.
# Preserve exact-size graphs (including T=1) and the eager path.
group_frames = frame_count
if coalesce_tails and finished:
# Only terminal rows may be padded: their state is reset after
# the step, and the output is cropped to the exact frame count.
group_frames = graph._select_frame_size(frame_count, allow_padding=True) or frame_count
grouped.setdefault(group_frames, []).append((output_index, request_id, slot, codes_nq_t, finished))
if fast is not None and grouped:
fast.gate()
for group in grouped.values():
plan = {slot: codes_nq_t for _, _, slot, codes_nq_t, _ in group}
terminal_slots = {slot for _, _, slot, _, finished in group if finished}
decoded = session.step(plan, terminal_slots=terminal_slots)
step_buffers = (
{
slot: output_buffers[output_index]
for output_index, _, slot, _, _ in group
if output_buffers is not None and output_index in output_buffers
}
if output_buffers is not None
else None
)
decoded = session.step(plan, terminal_slots=terminal_slots, output_buffers=step_buffers)
for output_index, request_id, slot, _, finished in group:
wav = decoded.get(slot)
if wav is not None:
outputs[output_index] = wav
prefix = unsent_first_audio.pop(output_index, None)
outputs[output_index] = torch.cat((prefix, wav), dim=-1) if prefix is not None else wav
if finished:
self._finish_stream_request(
request_id,
@@ -625,6 +956,7 @@ class MossTTSCodecDecoder(nn.Module):
codes_nq_t: torch.Tensor,
*,
terminal: bool = False,
output_buffer: torch.Tensor | None = None,
) -> torch.Tensor | None:
if codes_nq_t.numel() == 0:
return None
@@ -633,13 +965,24 @@ class MossTTSCodecDecoder(nn.Module):
for start in range(0, int(codes_nq_t.shape[1]), max_step_frames):
chunk = codes_nq_t[:, start : start + max_step_frames]
is_terminal_chunk = terminal and start + max_step_frames >= int(codes_nq_t.shape[1])
chunk_samples = int(chunk.shape[1]) * self._codec.downsample_rate
chunk_buffer = (
output_buffer[
..., start * self._codec.downsample_rate : start * self._codec.downsample_rate + chunk_samples
]
if output_buffer is not None
else None
)
decoded = session.step(
{slot: chunk},
terminal_slots={slot} if is_terminal_chunk else None,
output_buffers={slot: chunk_buffer} if chunk_buffer is not None else None,
)
wav = decoded.get(slot)
if wav is not None:
parts.append(wav)
if output_buffer is not None:
return output_buffer
if not parts:
return None
return torch.cat(parts, dim=-1) if len(parts) > 1 else parts[0]
@@ -665,29 +1008,30 @@ class MossTTSCodecDecoder(nn.Module):
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)
if slot is None:
continue
if session is not None:
self._finish_stream_request(request_id, session, slot)
else:
self._stream_req_slots.pop(request_id, None)
# Cancellation and terminal cleanup share the decoder's stream order;
# a reused slot must not be reset ahead of an outstanding decode.
context = torch.cuda.stream(self._codec_stream) if self._codec_stream is not None else nullcontext()
with context:
session = self._stream_session
for req_id in finished_req_ids:
request_id = str(req_id)
fast = self._first_chunk_fast_path
slot = (
fast.get_request_slot(request_id, self._stream_req_slots)
if fast is not None
else self._stream_req_slots.get(request_id)
)
if slot is None:
continue
if fast is not None:
fast.order_after(slot)
fast.forget(request_id)
if session is not None:
self._finish_stream_request(request_id, session, slot)
else:
self._stream_req_slots.pop(request_id, None)
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_extra_cfg(self) -> dict:
def _connector_extra(self) -> dict[str, Any]:
model_cfg = getattr(self.vllm_config, "model_config", None)
connector_cfg = getattr(model_cfg, "stage_connector_config", None)
if isinstance(connector_cfg, dict):
@@ -696,6 +1040,9 @@ class MossTTSCodecDecoder(nn.Module):
extra_cfg = getattr(connector_cfg, "extra", None)
return extra_cfg if isinstance(extra_cfg, dict) else {}
def _connector_int(self, name: str, default: int = 0) -> int:
return int(self._connector_extra().get(name, default))
def _streaming_graph_batch_sizes_from_compilation_config(self) -> list[int]:
if getattr(self.vllm_config.model_config, "enforce_eager", True):
return []
@@ -816,15 +1163,33 @@ class MossTTSCodecDecoder(nn.Module):
codec.decoder.to(dtype=torch.bfloat16)
attention_backend = getattr(self.vllm_config.model_config.hf_config, "codec_attention_backend", "sdpa")
if attention_backend != "sdpa":
if attention_backend != "triton" or device.type != "cuda":
if attention_backend not in {"triton", "triton_slot"} or device.type != "cuda":
raise ValueError(f"Unsupported codec attention backend/device: {attention_backend}/{device.type}")
from vllm_omni.model_executor.models.moss_tts.audio_tokenizer_v2 import MossAudioTokenizerMultiheadAttention
from vllm_omni.model_executor.models.moss_tts.streaming_attention import masked_attention
if attention_backend == "triton_slot":
from vllm_omni.model_executor.models.moss_tts.slot_attention import (
slot_ring_attention,
slot_ring_attention_rows,
)
skip_empty_tiles = bool(self._connector_int("codec_skip_empty_attention_tiles", default=0))
fused_slots = bool(self._connector_int("codec_fused_slot_attention", default=0))
if skip_empty_tiles and fused_slots:
raise ValueError("codec_skip_empty_attention_tiles requires unfused slot attention")
if skip_empty_tiles:
slot_ring_attention = partial(slot_ring_attention, skip_empty_tiles=True)
logger.info("MOSS codec slot attention: skipping empty tiles for T <= 32")
for module in codec.decoder.modules():
if isinstance(module, MossAudioTokenizerMultiheadAttention):
module._streaming_attention = masked_attention
logger.info("Enabled Triton masked attention for the streaming codec decoder")
if attention_backend == "triton_slot":
module._slot_attention = slot_ring_attention
if self._connector_int("codec_fused_slot_attention", default=0):
module._slot_attention_rows = slot_ring_attention_rows
logger.info("Enabled codec attention backend=%s", attention_backend)
build_decode_lut = getattr(codec.quantizer, "build_decode_lut", None)
if callable(build_decode_lut):
lut_dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
@@ -21,7 +21,8 @@ checkpoint 1:1 so ``load_weights()`` needs no remapping.
from __future__ import annotations
from collections.abc import Sequence
import os
from collections.abc import Callable, Sequence
import torch
import torch.nn as nn
@@ -52,6 +53,7 @@ class _MossTTSLocalAttention(nn.Module):
self.n_head = n_head
self.head_dim = hidden_size // n_head
self.embed_dim = hidden_size
self._short_attention = os.environ.get("VLLM_OMNI_MOSS_LOCAL_SHORT_ATTN", "0") == "1"
self.c_attn = nn.Linear(hidden_size, 3 * hidden_size, bias=True)
self.c_proj = nn.Linear(hidden_size, hidden_size, bias=True)
inv_freq = 1.0 / (rope_base ** (torch.arange(0, self.head_dim, 2, dtype=torch.float32) / self.head_dim))
@@ -134,7 +136,19 @@ class _MossTTSLocalAttention(nn.Module):
v_cache[:, :, position : position + 1].copy_(value)
key = k_cache[:, :, : position + 1]
value = v_cache[:, :, : position + 1]
attn_output = F.scaled_dot_product_attention(query, key, value, is_causal=kv_cache is None)
if (
self._short_attention
and kv_cache is not None
and query.is_cuda
and query.dtype == torch.bfloat16
and self.head_dim <= 128
and key.shape[2] <= 16
):
from vllm_omni.model_executor.models.moss_tts.local_short_attention import local_short_attention
attn_output = local_short_attention(query, key, value)
else:
attn_output = F.scaled_dot_product_attention(query, key, value, is_causal=kv_cache is None)
attn_output = attn_output.transpose(1, 2).reshape(batch_size, seq_len, self.embed_dim)
return self.c_proj(attn_output)
@@ -193,6 +207,7 @@ class MossTTSLocalDepthTransformer(nn.Module):
self.h = nn.ModuleList([_MossTTSLocalBlock(self.hidden_size, n_head, inner_size, rope_base, eps)])
self.ln_f = nn.LayerNorm(self.hidden_size, eps=eps)
self._compiled_forward_prefix = None
self._compiled_audio_sampler = None
def _forward_prefix(
self,
@@ -216,6 +231,14 @@ class MossTTSLocalDepthTransformer(nn.Module):
dynamic=True,
options={"epilogue_fusion": False},
)
if os.environ.get("VLLM_OMNI_MOSS_LOCAL_COMPILE_AUDIO_SAMPLER", "0") == "1":
# Keep the existing top-k/top-p algorithm and torch RNG. Explicit
# per-request generators use the original helper below. The
# binary continue/stop head is deliberately unchanged.
self._compiled_audio_sampler = torch.compile(
_sample_token, fullgraph=True, dynamic=True, options={"fallback_random": True}
)
logger.info("MOSS-TTS Local compiled audio-channel sampler enabled")
logger.info("MOSS-TTS local depth frame-local KV execution enabled with torch.compile")
def _run_prefix(
@@ -239,6 +262,7 @@ class MossTTSLocalDepthTransformer(nn.Module):
do_sample: bool,
generator: torch.Generator | None,
generators: Sequence[torch.Generator | None] | None = None,
sample_token: Callable[..., torch.Tensor] | None = None,
) -> torch.Tensor:
"""Compute channel logits, apply repetition penalty, sample, store."""
channel_logits = audio_lm_heads[channel_index](local_hidden).float()
@@ -250,7 +274,7 @@ 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_token = (sample_token or _sample_token)(
channel_logits,
temperature,
top_k,
@@ -351,6 +375,7 @@ class MossTTSLocalDepthTransformer(nn.Module):
do_sample=do_sample,
generator=generator,
generators=generators,
sample_token=self._compiled_audio_sampler if generator is None and generators is None else None,
)
if channel_index + 1 < n_vq:
@@ -37,6 +37,7 @@ from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_local_depth impo
)
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.platforms import current_omni_platform
from vllm_omni.worker.sampling_utils import get_tts_local_seed
logger = init_logger(__name__)
@@ -1675,6 +1676,31 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
return input_embeds_out, output_codes
# ------------------------------------------------------------------
# MRV2 reads model-owned capabilities; retain the V1 hook and platform
# graph-safety setting as the canonical implementation.
mtp = talker_mtp
get_mtp_seed = staticmethod(get_tts_local_seed)
def create_mrv2_model_state(self, vllm_config, encoder_cache, device):
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
state_cls = OmniModelState
if getattr(self.config, "mrv2_gpu_slot_state", False):
from .local_model_state import MossLocalModelState
state_cls = MossLocalModelState
return state_cls(vllm_config, self, encoder_cache, device)
@property
def mtp_output_key(self) -> tuple[str, str]:
"""Return the request-buffer destination for one local audio frame."""
return self.talker_mtp_output_key
@property
def mtp_graph_safe(self) -> bool:
"""Honor the same platform graph-safety override as the V1 runner."""
return self.talker_mtp_graph_safe
# Package runner-generated audio frames
# ------------------------------------------------------------------
@@ -97,6 +97,7 @@ MOSS_TTS_LOCAL_PIPELINE = PipelineConfig(
StagePipelineConfig(
stage_id=0,
model_stage="moss_tts_local",
supports_native_mrv2_data_plane=True,
execution_type=StageExecutionType.LLM_AR,
input_sources=(),
owns_tokenizer=True,
@@ -114,6 +115,7 @@ MOSS_TTS_LOCAL_PIPELINE = PipelineConfig(
StagePipelineConfig(
stage_id=1,
model_stage="moss_tts_local_codec",
supports_native_mrv2_data_plane=True,
execution_type=StageExecutionType.LLM_GENERATION,
input_sources=(0,),
final_output=True,
@@ -25,7 +25,10 @@ so importing it from the API-server process does not pull the talker/codec.
from __future__ import annotations
import asyncio
import contextlib
import hashlib
import os
from collections import OrderedDict
from collections.abc import Awaitable, Callable
from functools import lru_cache
from typing import Any
@@ -35,22 +38,93 @@ import torch
from numpy.typing import NDArray
from vllm.logger import init_logger
from vllm_omni.model_executor.models.moss_tts.reference_encoder_graphs import (
FINE_BATCH_SIZES,
FINE_BUCKET_SECONDS,
MossReferenceEncoderGraphs,
install_windowed_attention,
)
from vllm_omni.model_executor.models.moss_tts.shared_reference_encoder import (
SharedReferenceEncoderClient,
SharedReferenceEncoderHost,
SharedReferenceEncoderStartupError,
elect_host,
)
logger = init_logger(__name__)
# Coalescing defaults. Kept as module constants (not env/CLI) to match the
# ``_REF_AUDIO_RESOLVE_CACHE_MAX_*`` convention in serving_speech.py.
_REF_ENCODE_BATCH_WINDOW_MS = 10.0
_REF_ENCODE_MAX_BATCH = 8
_REF_ENCODE_MAX_BATCH = 32
# Experiment switches for cold-reference latency (see the A/B notes):
# batch window in ms; batches in flight per API process with a shared encoder;
# "high" puts reference encodes on the highest-priority CUDA stream.
_REF_ENCODE_WINDOW_ENV = "VLLM_OMNI_MOSS_REF_BATCH_WINDOW_MS"
_REF_ENCODE_INFLIGHT_ENV = "VLLM_OMNI_MOSS_REF_INFLIGHT"
_REF_ENCODE_PRIORITY_ENV = "VLLM_OMNI_MOSS_REF_STREAM_PRIORITY"
# "1" resamples and loudness-normalizes clips on the encoder's GPU instead of
# the API process's CPU (not bit-identical to the CPU path).
_REF_ENCODE_GPU_PREP_ENV = "VLLM_OMNI_MOSS_REF_GPU_PREP"
_INT32_MAX = 2**31
# Inline (data: URI) references already resolved and encoded, keyed by the
# serving resolve key. Codes are small; the URI itself is not retained.
_INLINE_INDEX_MAX_ENTRIES = 4096
# Optional directory (e.g. under /dev/shm) that API processes of one server
# share, so an inline reference encoded by any frontend is reused by all.
_SHARED_CODES_DIR_ENV = "VLLM_OMNI_MOSS_REF_CODES_SHARED_DIR"
# "0" keeps one reference encoder per API process even with a shared directory.
_SHARED_ENCODER_ENV = "VLLM_OMNI_MOSS_REF_SHARED_ENCODER"
_shared_role: str | None = None
# Concurrent encode workers in the shared host, each with its own CUDA graphs.
_SHARED_WORKERS_ENV = "VLLM_OMNI_MOSS_REF_ENCODER_WORKERS"
_SHARED_WORKER_BATCH_SECONDS = 96.0
def shared_encoder_role() -> str:
"""``"host"``, ``"client"`` or ``""`` (no shared encoder) for this process.
Decided once, before the processor places its tokenizer on the GPU, so
client processes can leave it on the CPU.
"""
global _shared_role
if _shared_role is None:
shared_dir = os.environ.get(_SHARED_CODES_DIR_ENV)
if not shared_dir or os.environ.get(_SHARED_ENCODER_ENV, "1") == "0":
_shared_role = ""
else:
os.makedirs(shared_dir, exist_ok=True)
_shared_role = "host" if elect_host(shared_dir) else "client"
return _shared_role
# Opt in to reference graphs; existing deployments keep the eager encoder.
_REF_ENCODE_GRAPHS_ENV = "VLLM_OMNI_MOSS_REF_GRAPHS"
def reference_graphs_enabled() -> bool:
return os.environ.get(_REF_ENCODE_GRAPHS_ENV, "0") == "1"
def reference_stream_priority() -> int:
"""CUDA stream priority for reference encodes (lower is more urgent)."""
if os.environ.get(_REF_ENCODE_PRIORITY_ENV, "") == "high" and torch.cuda.is_available():
return torch.cuda.Stream.priority_range()[1]
return 0
# "sdpa" keeps the tokenizer's dense masked SDPA attention. By default the
# reference encoder attends through a local-window flash kernel, the backend
# the tokenizer config selects (flash_attention_2) when flash-attn is present.
_REF_ENCODE_ATTN_ENV = "VLLM_OMNI_MOSS_REF_ATTN"
# "0" captures the encoder's eager kernels. By default the captured encoder is
# compiled first, which fuses its many small elementwise kernels.
_REF_ENCODE_COMPILE_ENV = "VLLM_OMNI_MOSS_REF_COMPILE"
_Waveform = NDArray[np.float32] | list[float]
def _sha1(s: str) -> str:
return hashlib.sha1((s or "").encode("utf-8")).hexdigest()
@lru_cache(maxsize=16)
def _reference_resampler(sr: int, sr_target: int) -> torch.nn.Module:
import torchaudio
@@ -72,6 +146,13 @@ def _prep_wav_sync(waveform: _Waveform, sr: int, sr_target: int) -> torch.Tensor
return wav
def _inline_reference_key(ref_str: str) -> str | None:
"""SHA-1 of a ``data:`` URI, matching the serving resolve key for it."""
if not isinstance(ref_str, str) or ref_str[:5].lower() != "data:":
return None
return hashlib.sha1(ref_str.encode("utf-8")).hexdigest()
def _to_compact_codes(codes: torch.Tensor) -> torch.Tensor:
"""Downcast RVQ codes to int32 for compact caching."""
codes = codes.detach().cpu().contiguous()
@@ -119,18 +200,26 @@ class _RefEncodeBatcher:
*,
window_ms: float,
max_batch: int,
max_inflight: int = 1,
):
# encode_batch_fn: sync, takes [(waveform, sr), ...], returns a list of
# (codes_tensor | Exception) aligned to the input order.
self._encode_batch_fn = encode_batch_fn
self._window_s = max(0.0, float(window_ms) / 1000.0)
self._max_batch = max(1, int(max_batch))
# Batches encoded at once. Clips that arrive while every slot is busy
# are queued and form the next batch.
self._max_inflight = max(1, int(max_inflight))
self._queue: asyncio.Queue | None = None
self._slots: asyncio.Semaphore | None = None
self._inflight: set[asyncio.Task] = set()
self._drainer: asyncio.Task | None = None
def _ensure_started(self) -> None:
if self._queue is None:
self._queue = asyncio.Queue()
if self._slots is None:
self._slots = asyncio.Semaphore(self._max_inflight)
if self._drainer is None or self._drainer.done():
self._drainer = asyncio.create_task(self._drain_loop())
@@ -141,16 +230,27 @@ class _RefEncodeBatcher:
return await fut
async def _drain_loop(self) -> None:
assert self._queue is not None
assert self._queue is not None and self._slots is not None
while True:
first = await self._queue.get()
jobs = None
await self._slots.acquire()
try:
jobs = await self._coalesce(first)
await self._run_batch(jobs)
finally:
# Do not retain the completed batch while waiting for new work.
del first, jobs
jobs = await self._coalesce(await self._queue.get())
except BaseException:
self._slots.release()
raise
task = asyncio.create_task(self._run_and_release(jobs))
self._inflight.add(task)
task.add_done_callback(self._inflight.discard)
# Do not retain the batch here while waiting for new work.
del jobs
async def _run_and_release(self, jobs: list[tuple[_Waveform, int, asyncio.Future]]) -> None:
try:
await self._run_batch(jobs)
finally:
del jobs
assert self._slots is not None
self._slots.release()
async def _coalesce(self, first: tuple) -> list[tuple]:
"""Group ``first`` with jobs arriving within the batch window."""
@@ -200,6 +300,8 @@ class _RefEncodeBatcher:
except (asyncio.CancelledError, Exception): # noqa: BLE001
pass
self._drainer = None
for task in list(self._inflight):
task.cancel()
class MossReferenceEncoder:
@@ -213,7 +315,7 @@ class MossReferenceEncoder:
n_vq: int,
sr_target: int,
speaker_cache: Any,
window_ms: float = _REF_ENCODE_BATCH_WINDOW_MS,
window_ms: float | None = None,
max_batch: int = _REF_ENCODE_MAX_BATCH,
):
self._processor = processor
@@ -224,8 +326,33 @@ class MossReferenceEncoder:
# model_type namespaces the whole family so a moss_tts server never
# collides with another model's speaker-cache entries.
self._model_type = f"moss_tts_{variant}_nq{int(n_vq)}"
self._inflight: dict[str, asyncio.Task] = {}
self._batcher = _RefEncodeBatcher(self._encode_batch_sync, window_ms=window_ms, max_batch=max_batch)
self._inflight: dict[str | tuple[str, str], asyncio.Task] = {}
self._inline_index: OrderedDict[str, torch.Tensor] = OrderedDict()
self._shared_codes_dir = os.environ.get(_SHARED_CODES_DIR_ENV) or None
if self._shared_codes_dir:
os.makedirs(self._shared_codes_dir, exist_ok=True)
# Realtime encodes with its own codec and never shares the encoder.
self._shares_encoder = bool(self._shared_codes_dir) and type(self)._encode_prepared is (
MossReferenceEncoder._encode_prepared
)
self._shared_host: SharedReferenceEncoderHost | None = None
self._shared_client: SharedReferenceEncoderClient | None = None
self._graphs: MossReferenceEncoderGraphs | None = None
self._attention_installed = False
self._graphs_enabled = reference_graphs_enabled()
if window_ms is None:
window_ms = float(os.environ.get(_REF_ENCODE_WINDOW_ENV, _REF_ENCODE_BATCH_WINDOW_MS))
# A shared encoder serves several batches at once (one per host
# worker); a process's own graphs replay one batch at a time.
inflight = 1
if self._shares_encoder and os.environ.get(_SHARED_ENCODER_ENV, "1") != "0":
inflight = max(1, int(os.environ.get(_REF_ENCODE_INFLIGHT_ENV, "1")))
self._local_stream: torch.cuda.Stream | None = None
self._gpu_prep = os.environ.get(_REF_ENCODE_GPU_PREP_ENV, "0") == "1"
self._gpu_resamplers: dict[torch.device, torch.nn.Module] = {}
self._batcher = _RefEncodeBatcher(
self._encode_batch_sync, window_ms=window_ms, max_batch=max_batch, max_inflight=inflight
)
def _make_cache_key(self, name: str, created_at: int) -> tuple:
return self._speaker_cache.make_cache_key(name, model_type=self._model_type, created_at=int(created_at))
@@ -251,6 +378,7 @@ class MossReferenceEncoder:
caller salts those requests with ``voice_created_at`` instead).
"""
voice_name, created_at = _registered_voice(voice_name, voice_created_at)
inline_key: str | None = None
if voice_name:
# A named voice has a stable key that does not depend on the
@@ -260,6 +388,16 @@ class MossReferenceEncoder:
if cached is not None:
return _clone_out(cached["codes"]), None
else:
inline_key = _inline_reference_key(ref_str)
if inline_key is not None:
codes = self._inline_index.get(inline_key)
if codes is None:
codes = self._load_shared_codes(inline_key)
if codes is not None:
# A data: URI is immutable content, and its serving
# resolve key is this same digest: skip the resolve hop.
self._inline_index.move_to_end(inline_key)
return _clone_out(codes), inline_key
# Anonymous refs have no pre-resolve hot path: the content key must
# come from the resolve itself (mtime/size from a single stat) so an
# on-disk edit invalidates the cached codes; the flight body
@@ -267,17 +405,55 @@ class MossReferenceEncoder:
# when the resolve cache is warm. The flight key is the request-side
# reference (not the content hash), so concurrent requests for the
# same ref_str also share the resolve/download, not just the encode.
flight_key = "ref:" + _sha1(ref_str)
# Key by the locator itself: equality is exact, and hashing a
# large inline data URI once per request object is much cheaper
# than a separate SHA-1 of its UTF-8 copy on the event loop.
flight_key = ("ref", ref_str)
codes, resolve_key = await self._single_flight(
flight_key,
lambda: self._resolve_and_encode(ref_str, resolve_ref_audio, get_artifact_key, voice_name, created_at),
)
if not voice_name and inline_key is not None and resolve_key == inline_key:
# Only index when the resolver's key is exactly this digest.
self._remember_inline(inline_key, codes)
self._store_shared_codes(inline_key, codes)
return _clone_out(codes), resolve_key
def _remember_inline(self, key: str, codes: torch.Tensor) -> None:
self._inline_index[key] = codes
self._inline_index.move_to_end(key)
while len(self._inline_index) > _INLINE_INDEX_MAX_ENTRIES:
self._inline_index.popitem(last=False)
def _load_shared_codes(self, key: str) -> torch.Tensor | None:
if not self._shared_codes_dir:
return None
try:
array = np.load(os.path.join(self._shared_codes_dir, key + ".npy"), allow_pickle=False)
except (OSError, ValueError):
return None
if array.ndim != 2 or array.shape[1] != self._n_vq or array.dtype not in (np.int32, np.int64):
return None
codes = torch.from_numpy(array)
self._remember_inline(key, codes)
return codes
def _store_shared_codes(self, key: str, codes: torch.Tensor) -> None:
if not self._shared_codes_dir:
return
path = os.path.join(self._shared_codes_dir, key + ".npy")
tmp = f"{path}.{os.getpid()}.tmp"
try:
with open(tmp, "wb") as handle:
np.save(handle, codes.detach().cpu().numpy(), allow_pickle=False)
os.replace(tmp, path)
except OSError:
logger.warning("MOSS ref codes: could not write shared entry %s", path, exc_info=True)
async def _single_flight(
self,
flight_key: str,
flight_key: str | tuple[str, str],
start_flight: Callable[[], Awaitable[tuple[torch.Tensor, str]]],
) -> tuple[torch.Tensor, str]:
"""Join the in-flight encode for ``flight_key``, starting one if absent.
@@ -300,7 +476,7 @@ class MossReferenceEncoder:
# a duplicate resolve/encode instead of joining this one. Identity
# guard: only drop the slot if it still holds *our* task (a later
# request may have replaced it after ours completed).
def _retire(t: asyncio.Task, key: str = flight_key) -> None:
def _retire(t: asyncio.Task, key: str | tuple[str, str] = flight_key) -> None:
if self._inflight.get(key) is t:
self._inflight.pop(key, None)
@@ -350,29 +526,228 @@ class MossReferenceEncoder:
prepared_idx: list[int] = []
for i, (waveform, sr) in enumerate(payload):
try:
prepared.append(_prep_wav_sync(waveform, sr, self._sr_target))
prepared.append(self._encoder_input(_prep_wav_sync(waveform, sr, self._sr_target)))
prepared_idx.append(i)
except Exception as exc: # noqa: BLE001 — isolate this clip's failure
results[i] = exc
if not prepared:
return results
try:
codes_list = self._encode_prepared(prepared)
for local_i, orig_i in enumerate(prepared_idx):
results[orig_i] = codes_list[local_i]
except Exception: # noqa: BLE001 — batch forward failed; retry item-by-item
logger.warning("MOSS ref batch encode (n=%d) failed; falling back to per-item", len(prepared))
for local_i, orig_i in enumerate(prepared_idx):
try:
results[orig_i] = self._encode_prepared([prepared[local_i]])[0]
except Exception as exc: # noqa: BLE001 — isolate this clip's failure
results[orig_i] = exc
for orig_i, codes in zip(prepared_idx, self._encode_shared_or_local(prepared)):
results[orig_i] = codes
return results
def _encode_prepared(self, prepared: list[torch.Tensor]) -> list[torch.Tensor]:
with torch.no_grad():
return self._processor.encode_audios_from_wav(prepared, sampling_rate=self._sr_target, n_vq=self._n_vq)
def prepare(self) -> None:
"""Capture the CUDA graphs (or start the shared encoder) before the first request."""
role = shared_encoder_role() if self._shares_encoder else ""
if role == "host":
self._host().wait_until_ready()
elif role == "client":
if self._shared_client is None:
self._shared_client = SharedReferenceEncoderClient(self._shared_codes_dir)
self._shared_client.wait_until_ready()
elif role == "":
self._reference_graphs()
def _host(self) -> SharedReferenceEncoderHost:
if self._shared_host is None:
# Graph capture runs in the background; clips queue until it ends.
self._shared_host = SharedReferenceEncoderHost(
self._shared_codes_dir,
self._host_workers(),
stream_priority=reference_stream_priority(),
batch_window_ms=float(os.environ.get("VLLM_OMNI_MOSS_REF_HOST_WINDOW_MS", "0")),
)
return self._shared_host
def _encode_shared_or_local(self, prepared: list[torch.Tensor]) -> list:
role = shared_encoder_role() if self._shares_encoder else ""
if role == "host":
return self._host().encode(prepared)
if role == "client":
if self._shared_client is None:
self._shared_client = SharedReferenceEncoderClient(self._shared_codes_dir)
try:
return self._shared_client.encode(prepared)
except SharedReferenceEncoderStartupError:
raise
except Exception: # noqa: BLE001 — the host is unreachable: encode here
logger.warning("MOSS shared reference encoder unavailable; encoding locally", exc_info=True)
self._place_tokenizer_for_local_encode()
with self._local_stream_context():
return self._encode_local(prepared)
def _local_stream_context(self):
tokenizer = getattr(self._processor, "audio_tokenizer", None)
params = next(tokenizer.parameters(), None) if isinstance(tokenizer, torch.nn.Module) else None
if params is None or params.device.type != "cuda":
return contextlib.nullcontext()
if self._local_stream is None:
self._local_stream = torch.cuda.Stream(device=params.device, priority=reference_stream_priority())
return torch.cuda.stream(self._local_stream)
def _host_workers(self) -> list:
"""Encode workers of the shared host, each with its own graphs and stream."""
count = max(1, int(os.environ.get(_SHARED_WORKERS_ENV, "4")))
if not self._graph_capable():
return [(self._encode_local, None)] * count
workers = []
for _ in range(count):
graphs = MossReferenceEncoderGraphs(
self._processor.audio_tokenizer,
n_vq=self._n_vq,
batch_sizes=FINE_BATCH_SIZES,
bucket_seconds=FINE_BUCKET_SECONDS,
max_batch_seconds=_SHARED_WORKER_BATCH_SECONDS,
compile_core=self._compile_graphs(),
batched_transfer=os.environ.get("VLLM_OMNI_MOSS_REF_BATCHED_TRANSFER", "0") == "1",
singleton_bucket_seconds=self._singleton_bucket_seconds(),
)
workers.append(((lambda wavs, g=graphs: self._encode_local(wavs, g)), graphs.capture))
return workers
def _encode_local(self, prepared: list[torch.Tensor], graphs: MossReferenceEncoderGraphs | None = None) -> list:
"""One batched forward, retried per clip so one bad clip fails alone."""
try:
return list(self._encode_prepared(prepared, graphs))
except Exception: # noqa: BLE001 — batch forward failed; retry item-by-item
logger.warning("MOSS ref batch encode (n=%d) failed; falling back to per-item", len(prepared))
results: list = []
for wav in prepared:
try:
results.append(self._encode_prepared([wav], graphs)[0])
except Exception as exc: # noqa: BLE001 — isolate this clip's failure
results.append(exc)
return results
def _place_tokenizer_for_local_encode(self) -> None:
"""A client process keeps the tokenizer off the GPU until it must encode."""
tokenizer = getattr(self._processor, "audio_tokenizer", None)
device = getattr(self._processor, "_vllm_omni_ref_encoder_device", None)
if tokenizer is not None and device is not None and next(tokenizer.parameters()).device != device:
self._processor.audio_tokenizer = tokenizer.to(device)
def _encode_prepared(
self, prepared: list[torch.Tensor], graphs: MossReferenceEncoderGraphs | None = None
) -> list[torch.Tensor]:
# ``prepared`` clips come from _encoder_input, at ``_clip_rate()``.
graphs = graphs or self._reference_graphs()
if graphs is None:
with torch.no_grad():
return self._processor.encode_audios_from_wav(
prepared, sampling_rate=self._clip_rate(), n_vq=self._n_vq
)
codes = graphs.encode([self._finish_prepare(wav) for wav in prepared])
missing = [i for i, c in enumerate(codes) if c is None]
if missing:
with torch.no_grad():
eager = self._processor.encode_audios_from_wav(
[prepared[i] for i in missing], sampling_rate=self._clip_rate(), n_vq=self._n_vq
)
for i, c in zip(missing, eager):
codes[i] = c
return codes
def _graph_capable(self) -> bool:
"""True when the tokenizer can run graphs; installs windowed attention once."""
if not self._graphs_enabled:
return False
tokenizer = getattr(self._processor, "audio_tokenizer", None)
device = next(tokenizer.parameters()).device if tokenizer is not None else None
if device is None or device.type != "cuda" or not hasattr(tokenizer, "_codec_inference_autocast"):
self._graphs_enabled = False
return False
if not self._attention_installed and os.environ.get(_REF_ENCODE_ATTN_ENV, "flash") == "flash":
logger.info(
"MOSS reference encoder: %d attention layers use local-window flash attention",
install_windowed_attention(
tokenizer,
fa_version=int(os.environ.get("VLLM_OMNI_MOSS_REF_FA_VERSION", "2")),
cache_rope=os.environ.get("VLLM_OMNI_MOSS_REF_CACHE_ROPE", "0") == "1",
skip_padded_query_mask=os.environ.get("VLLM_OMNI_MOSS_REF_SKIP_PADDED_QUERY_MASK", "0") == "1",
),
)
self._attention_installed = True
return True
@staticmethod
def _compile_graphs() -> bool:
return os.environ.get(_REF_ENCODE_COMPILE_ENV, "1") != "0"
@staticmethod
def _singleton_bucket_seconds() -> tuple[float, ...] | None:
step_ms = int(os.environ.get("VLLM_OMNI_MOSS_REF_SINGLETON_BUCKET_MS", "0"))
if step_ms == 0:
return None
if not 80 <= step_ms <= 1000:
raise ValueError("MOSS reference singleton bucket step must be 80..1000 ms, or 0 to disable")
return tuple(ms / 1000 for ms in range(3000, 10001, step_ms))
def _reference_graphs(self) -> MossReferenceEncoderGraphs | None:
"""Capture on first use, on the encode worker thread that replays them."""
if self._graphs is None:
if not self._graph_capable():
return None
graphs = MossReferenceEncoderGraphs(
self._processor.audio_tokenizer,
n_vq=self._n_vq,
compile_core=self._compile_graphs(),
batched_transfer=os.environ.get("VLLM_OMNI_MOSS_REF_BATCHED_TRANSFER", "0") == "1",
singleton_bucket_seconds=self._singleton_bucket_seconds(),
)
graphs.capture()
self._graphs = graphs
return self._graphs
def _tokenizer_rate(self) -> int:
return int(getattr(getattr(self._processor, "model_config", None), "sampling_rate", self._sr_target))
def _encoder_input(self, wav: torch.Tensor) -> torch.Tensor:
"""A prepped clip at the tokenizer rate, one or two channels.
This is the costly half of the processor's per-clip preparation; it runs
in the process that received the request, even with a shared encoder.
"""
import torchaudio
if wav.ndim == 1:
wav = wav.unsqueeze(0)
if wav.shape[0] > 2:
wav = wav[:2]
if self._clip_rate() != self._sr_target:
# Channels resample independently, so a mono clip is resampled
# once before it is duplicated; the result is the same.
wav = torchaudio.functional.resample(wav, self._sr_target, self._tokenizer_rate())
return wav
def _clip_rate(self) -> int:
"""Sample rate of the clips _encoder_input hands to the encoder."""
return self._sr_target if self._gpu_prep else self._tokenizer_rate()
def _finish_prepare(self, wav: torch.Tensor) -> torch.Tensor:
"""The rest of the processor's preparation: two channels, normalized loudness."""
if self._gpu_prep:
device = next(self._processor.audio_tokenizer.parameters()).device
wav = wav.to(device)
if self._clip_rate() != self._tokenizer_rate():
wav = self._gpu_resampler(device)(wav)
if wav.shape[0] == 1:
wav = wav.repeat(2, 1)
return self._processor.loudness_normalize(wav)
def _gpu_resampler(self, device: torch.device) -> torch.nn.Module:
resampler = self._gpu_resamplers.get(device)
if resampler is None:
import torchaudio
# The same windowed-sinc filter as torchaudio.functional.resample's defaults.
resampler = torchaudio.transforms.Resample(self._sr_target, self._tokenizer_rate()).to(device)
self._gpu_resamplers[device] = resampler
return resampler
def _processor_prepare(self, wav: torch.Tensor) -> torch.Tensor:
"""Per-clip preparation of the processor's ``encode_audios_from_wav``."""
return self._finish_prepare(self._encoder_input(wav))
async def aclose(self) -> None:
"""Release the batcher drainer + any in-flight encodes (tests/shutdown)."""
@@ -385,7 +760,10 @@ class MossReferenceEncoder:
class MossRealtimeReferenceEncoder(MossReferenceEncoder):
"""Use the realtime codec with the shared reference cache and batcher."""
def _encode_prepared(self, prepared: list[torch.Tensor]) -> list[torch.Tensor]:
def _encoder_input(self, wav: torch.Tensor) -> torch.Tensor:
return wav # the realtime codec takes the clip at the working rate
def _encode_prepared(self, prepared: list[torch.Tensor], graphs: Any = None) -> list[torch.Tensor]:
with torch.no_grad():
encoded = self._processor.batch_encode([wav.squeeze(0) for wav in prepared], num_quantizers=self._n_vq)
return [
@@ -0,0 +1,399 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CUDA graphs for batched MOSS reference-audio encoding.
Reference clips are encoded by the upstream MOSS audio tokenizer that the
processor loads. Its eager forward is dominated by kernel launches for the
small batches that serving produces. This runner replays graphs of the same
encoder and quantizer modules, captured per ``(batch, length)`` bucket. For a
given padded input the codes are bit-identical to the tokenizer's own
``_encode_frame``. Only the per-clip crop, which needs host values, runs
outside the graph.
The encoder is causal, so padding a clip to its length bucket leaves its valid
frames unchanged up to kernel selection for the padded shape, the same effect
that batching already has when the tokenizer pads a batch to its longest clip.
"""
from __future__ import annotations
import math
import types
from dataclasses import dataclass
import torch
from vllm.logger import init_logger
logger = init_logger(__name__)
DEFAULT_BATCH_SIZES = (1, 2, 4, 8, 16, 32)
DEFAULT_BUCKET_SECONDS = (4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0)
# Fine steps keep padding small: a padded row or padded time is encoded in
# full. Worth the extra graphs where several encoders share one GPU.
FINE_BATCH_SIZES = (1, 2, 3, 4, 5, 6, 8)
FINE_BUCKET_SECONDS = (3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 12.0, 14.0, 16.0, 20.0)
# Graphs are captured only while batch * bucket stays within this much audio,
# which bounds the shared graph memory pool by its largest batch.
DEFAULT_MAX_BATCH_SECONDS = 256.0
@torch.compile(dynamic=False)
def _reference_rope_factors(seqlen: int, head_dim: int, max_period: float, device: torch.device):
# Use the same compiled FP32 arithmetic as the encoder. Eager libdevice
# sin/cos and compiled sin/cos can round differently near a VQ boundary.
ds = torch.arange(head_dim // 2, device=device, dtype=torch.float32)
freqs = torch.exp(ds * (-math.log(max_period) * 2 / head_dim))
angles = torch.arange(seqlen, device=device, dtype=torch.float32)[:, None] * freqs[None, :]
return torch.cos(angles), torch.sin(angles)
def _cached_reference_rope(self, q: torch.Tensor, k: torch.Tensor, cache: dict):
if self.rope is None:
return q, k
batch, heads, seqlen, dim = q.shape
key = (dim, self.rope.max_period, q.device)
if key not in cache or cache[key][0].shape[0] < seqlen:
cache[key] = _reference_rope_factors(seqlen, dim, self.rope.max_period, q.device)
cos, sin = cache[key]
cos, sin = cos[:seqlen], sin[:seqlen]
qr, qi = q.view(batch, heads, seqlen, dim // 2, 2).unbind(-1)
kr, ki = k.view(batch, heads, seqlen, dim // 2, 2).unbind(-1)
qr, qi, kr, ki = qr.float(), qi.float(), kr.float(), ki.float()
# Preserve the original arithmetic, casts, and B/H/T/D output layout.
qo = torch.stack(((qr * cos - qi * sin).to(q.dtype), (qr * sin + qi * cos).to(q.dtype)), dim=-1)
ko = torch.stack(((kr * cos - ki * sin).to(k.dtype), (kr * sin + ki * cos).to(k.dtype)), dim=-1)
return qo.view(batch, heads, seqlen, dim), ko.view(batch, heads, seqlen, dim)
def _windowed_attention(
self,
x: torch.Tensor,
input_lengths: torch.Tensor,
*,
sdpa,
fa_version: int = 2,
rope_cache: dict | None = None,
skip_padded_query_mask: bool = False,
) -> torch.Tensor:
"""The tokenizer's non-streaming attention with a local-window flash kernel.
Same projection, RoPE and padded-row zeroing as the tokenizer's SDPA path.
That path masks each query to ``context`` keys (distance 0 .. context-1)
inside a dense block; the flash kernel visits only that window.
"""
batch, seqlen, _ = x.shape
q, k, v = self._project_qkv(x)
q, k = self._apply_dense_rope(q, k) if rope_cache is None else _cached_reference_rope(self, q, k, rope_cache)
if q.dtype not in (torch.bfloat16, torch.float16):
return sdpa(x, input_lengths)
from vllm.vllm_flash_attn import flash_attn_varlen_func
heads, dim = q.shape[1], q.shape[-1]
def packed(t: torch.Tensor) -> torch.Tensor:
return t.transpose(1, 2).reshape(batch * seqlen, heads, dim)
starts = torch.arange(0, (batch + 1) * seqlen, seqlen, device=x.device, dtype=torch.int32)
window = [self.context - 1, 0] if self.causal and self.context is not None else [-1, -1]
out = flash_attn_varlen_func(
packed(q),
packed(k),
packed(v),
max_seqlen_q=seqlen,
cu_seqlens_q=starts,
max_seqlen_k=seqlen,
cu_seqlens_k=starts,
causal=self.causal,
window_size=window,
fa_version=fa_version,
).view(batch, seqlen, heads, dim)
if not skip_padded_query_mask:
valid = (torch.arange(seqlen, device=x.device).view(1, seqlen) < input_lengths.view(-1, 1)).view(
batch, seqlen, 1, 1
)
out = torch.where(valid, out, torch.zeros((), device=out.device, dtype=out.dtype))
return out.reshape(batch, seqlen, self.embed_dim)
def _validate_unmasked_reference_encoder(tokenizer: torch.nn.Module) -> None:
"""Restrict the optimization to the audited HF encoder's valid-prefix contract.
Causal attention and pointwise transformer operations cannot read future
padded rows. Its patch-downsample floors lengths, so every valid output
group consists entirely of valid input rows. The quantizer is pointwise
and the caller crops codes by those lengths. Padded *outputs* may differ.
This argument does not apply to the streaming decoder or to upsampling.
"""
transformers = 0
for module in tokenizer.encoder:
name = type(module).__name__
if name == "MossAudioTokenizerPatchedPretransform" and module.is_downsample:
continue
if name == "MossAudioTokenizerProjectedTransformer":
layers = module.transformer.layers
if layers and all(layer.self_attn.causal for layer in layers):
transformers += 1
continue
raise ValueError("Unmasked reference queries require the causal MOSS patch-downsample encoder")
if not transformers:
raise ValueError("Unmasked reference queries require at least one causal MOSS transformer")
if type(tokenizer.quantizer).__name__ not in {
"MossAudioTokenizerResidualVQ",
"MossAudioTokenizerResidualLFQ",
}:
raise ValueError("Unmasked reference queries require the pointwise MOSS quantizer")
for module in tokenizer.quantizer.modules():
if isinstance(module, torch.nn.Conv1d) and module.kernel_size != (1,):
raise ValueError("Unmasked reference queries do not support temporal quantizer convolutions")
def install_windowed_attention(
tokenizer: torch.nn.Module,
*,
fa_version: int = 2,
cache_rope: bool = False,
skip_padded_query_mask: bool = False,
) -> int:
"""Route the encoder's non-streaming attention through a local-window flash kernel."""
if skip_padded_query_mask:
_validate_unmasked_reference_encoder(tokenizer)
logger.info("MOSS reference encoder: skipping padded query masks (valid-prefix outputs only)")
count = 0
rope_cache = {} if cache_rope else None
if cache_rope:
tokenizer._moss_reference_rope_cache = rope_cache
tokenizer._moss_reference_rope_prepared_samples = 0
for module in tokenizer.encoder.modules():
sdpa = getattr(module, "_forward_non_streaming_sdpa", None)
if sdpa is None or not hasattr(module, "_project_qkv") or not getattr(module, "causal", False):
continue
def attention(self, x, input_lengths, _sdpa=sdpa):
return _windowed_attention(
self,
x,
input_lengths,
sdpa=_sdpa,
fa_version=fa_version,
rope_cache=rope_cache,
skip_padded_query_mask=skip_padded_query_mask,
)
module._forward_non_streaming_sdpa = types.MethodType(attention, module)
count += 1
return count
@dataclass
class _Graph:
graph: torch.cuda.CUDAGraph
audio: torch.Tensor # (B, C, T) float32 input
lengths: torch.Tensor # (B,) int64 input, in samples per channel
codes: torch.Tensor # (n_vq, B, frames) output
code_lengths: torch.Tensor # (B,) output
class MossReferenceEncoderGraphs:
"""Encode prepared reference waveforms with captured tokenizer graphs."""
def __init__(
self,
tokenizer: torch.nn.Module,
*,
n_vq: int,
batch_sizes: tuple[int, ...] = DEFAULT_BATCH_SIZES,
bucket_seconds: tuple[float, ...] = DEFAULT_BUCKET_SECONDS,
max_batch_seconds: float = DEFAULT_MAX_BATCH_SECONDS,
compile_core: bool = False,
batched_transfer: bool = False,
singleton_bucket_seconds: tuple[float, ...] | None = None,
) -> None:
self._tokenizer = tokenizer
self._compile_core = compile_core
self._batched_transfer = batched_transfer
self._n_vq = int(n_vq)
self._device = next(tokenizer.parameters()).device
self._channels = int(tokenizer.number_channels)
frame = int(tokenizer.downsample_rate)
sampling_rate = int(tokenizer.sampling_rate)
# Whole frames, so the tokenizer's own frame padding is a no-op.
self._buckets = sorted({int(math.ceil(s * sampling_rate / frame)) * frame for s in bucket_seconds})
self._singleton_buckets = sorted(
set(self._buckets)
| {int(math.ceil(s * sampling_rate / frame)) * frame for s in (singleton_bucket_seconds or ())}
)
self._batch_sizes = sorted({int(b) for b in batch_sizes if int(b) > 0})
self._max_batch_samples = float(max_batch_seconds) * sampling_rate
self._graphs: dict[tuple[int, int], _Graph] = {}
self._pool = None
self._transfer_buffers = None
@property
def max_batch(self) -> int:
return self._batch_sizes[-1]
@property
def captured(self) -> list[tuple[int, int]]:
return sorted(self._graphs)
def _core(self, audio: torch.Tensor, lengths: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
# Mirrors the tokenizer's _encode_frame without its host-side crop.
tokenizer = self._tokenizer
values, value_lengths = tokenizer._flatten_channels_for_codec(audio, lengths)
with tokenizer._codec_inference_autocast():
hidden, hidden_lengths = values, value_lengths
for module in tokenizer.encoder:
hidden, hidden_lengths = module(hidden, hidden_lengths)
_, codes, code_lengths = tokenizer.quantizer(hidden.float(), hidden_lengths, self._n_vq)
return codes, code_lengths
@torch.no_grad()
def capture(self) -> None:
if self._device.type != "cuda" or self._graphs:
return
self._pool = torch.cuda.graph_pool_handle()
stream = torch.cuda.Stream(device=self._device)
prepared = getattr(self._tokenizer, "_moss_reference_rope_prepared_samples", None)
max_samples = max(self._singleton_buckets)
if prepared is not None and prepared < max_samples:
# Prime immutable FP32 factors at the largest length before any
# capture or compilation. Smaller lengths use a prefix of the
# same table; the dictionary stays fixed throughout capture and
# workers share tables without introducing dynamic-shape guards
# for a separate dictionary key at each sequence length.
audio = torch.zeros(1, self._channels, max_samples, device=self._device)
lengths = torch.full((1,), max_samples, dtype=torch.long, device=self._device)
self._core(audio, lengths)
self._tokenizer._moss_reference_rope_prepared_samples = max_samples
core = self._core
if self._compile_core:
# Fuses the encoder's many small elementwise kernels (casts, RoPE,
# residuals). Only capture runs the compiled code; replays do not.
core = torch.compile(self._core, dynamic=True)
# Each encoder stage is its own module shape: allow a compile per stage.
with torch._dynamo.config.patch(recompile_limit=64):
self._capture_all(core, stream)
if self._batched_transfer and self._graphs:
# One reusable slab per worker, rather than one per graph bucket.
entries = list(self._graphs.values())
self._transfer_buffers = (
torch.empty(max(e.audio.numel() for e in entries), dtype=entries[0].audio.dtype, pin_memory=True),
torch.empty(max(e.lengths.numel() for e in entries), dtype=entries[0].lengths.dtype, pin_memory=True),
torch.empty(max(e.codes.numel() for e in entries), dtype=entries[0].codes.dtype, pin_memory=True),
torch.empty(
max(e.code_lengths.numel() for e in entries), dtype=entries[0].code_lengths.dtype, pin_memory=True
),
torch.cuda.Event(),
)
logger.info(
"MOSS reference encoder CUDA graphs: batch=%s buckets(s)=%s compiled=%s captured=%d",
self._batch_sizes,
[round(s / int(self._tokenizer.sampling_rate), 2) for s in self._buckets],
self._compile_core,
len(self._graphs),
)
if self._singleton_buckets != self._buckets:
logger.info(
"MOSS reference encoder singleton buckets(s): %s",
[round(s / int(self._tokenizer.sampling_rate), 2) for s in self._singleton_buckets],
)
def _capture_all(self, core, stream: torch.cuda.Stream) -> None:
for batch in reversed(self._batch_sizes):
buckets = self._singleton_buckets if batch == 1 else self._buckets
for samples in reversed(buckets):
if batch > 1 and batch * samples > self._max_batch_samples:
continue
audio = torch.zeros(batch, self._channels, samples, device=self._device)
lengths = torch.full((batch,), samples, dtype=torch.long, device=self._device)
try:
stream.wait_stream(torch.cuda.current_stream(self._device))
with torch.cuda.stream(stream):
# Lazy library setup must happen before capture; one run does it.
core(audio, lengths)
torch.cuda.current_stream(self._device).wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, pool=self._pool, stream=stream):
codes, code_lengths = core(audio, lengths)
except Exception:
logger.warning(
"MOSS reference encoder graph (B=%d, samples=%d) not captured; using eager",
batch,
samples,
exc_info=True,
)
continue
self._graphs[(batch, samples)] = _Graph(graph, audio, lengths, codes, code_lengths)
@torch.no_grad()
def encode(self, wavs: list[torch.Tensor]) -> list[torch.Tensor | None]:
"""Codes ``(frames, n_vq)`` int64 on CPU per clip; None where no graph fits.
``wavs`` are prepared the way the processor prepares them: ``(C, T)``
float32 after channel handling, resampling and loudness normalization.
Clips are grouped longest first, so each replay pads to the bucket of
similar-length clips.
"""
out: list[torch.Tensor | None] = [None] * len(wavs)
order = sorted(range(len(wavs)), key=lambda i: -int(wavs[i].shape[-1]))
pos = 0
while pos < len(order):
longest = int(wavs[order[pos]].shape[-1])
# Fine singleton buckets reduce padding without fragmenting an
# already assembled batch into separate graph replays.
buckets = self._singleton_buckets if len(order) - pos == 1 else self._buckets
samples = next((s for s in buckets if s >= longest and (1, s) in self._graphs), None)
if samples is None:
pos += 1 # longer than every bucket: left to the caller
continue
widest = max(b for b in self._batch_sizes if (b, samples) in self._graphs)
rows = order[pos : pos + widest]
batch = next(b for b in self._batch_sizes if b >= len(rows) and (b, samples) in self._graphs)
for row, codes in zip(rows, self._replay(self._graphs[(batch, samples)], [wavs[i] for i in rows])):
out[row] = codes
pos += len(rows)
return out
def _replay(self, entry: _Graph, wavs: list[torch.Tensor]) -> list[torch.Tensor]:
if self._transfer_buffers is not None and all(w.device.type == "cpu" for w in wavs):
return self._replay_batched_transfer(entry, wavs)
count, batch, samples = len(wavs), entry.audio.shape[0], entry.audio.shape[-1]
# Unused rows encode full-length silence: a fully masked row would be
# all-NaN inside attention, and rows never mix.
entry.audio.zero_()
lengths = [int(w.shape[-1]) for w in wavs] + [samples] * (batch - count)
entry.lengths.copy_(torch.tensor(lengths, dtype=torch.long))
for row, wav in enumerate(wavs):
entry.audio[row, :, : wav.shape[-1]].copy_(wav)
entry.graph.replay()
codes = entry.codes[:, :count].cpu()
code_lengths = entry.code_lengths[:count].tolist()
return [codes[:, row, : int(code_lengths[row])].transpose(0, 1).contiguous().long() for row in range(count)]
def _replay_batched_transfer(self, entry: _Graph, wavs: list[torch.Tensor]) -> list[torch.Tensor]:
"""Stage one batch on pinned host memory, then wait once for its outputs."""
audio_slab, lengths_slab, codes_slab, code_lengths_slab, copied = self._transfer_buffers
count, batch, samples = len(wavs), entry.audio.shape[0], entry.audio.shape[-1]
audio = audio_slab[: entry.audio.numel()].view_as(entry.audio)
lengths = lengths_slab[:batch]
audio.zero_()
lengths.fill_(samples)
for row, wav in enumerate(wavs):
audio[row, :, : wav.shape[-1]].copy_(wav)
lengths[row] = wav.shape[-1]
entry.audio.copy_(audio, non_blocking=True)
entry.lengths.copy_(lengths, non_blocking=True)
entry.graph.replay()
result = entry.codes[:, :count]
codes = codes_slab[: result.numel()].view_as(result)
code_lengths = code_lengths_slab[:count]
codes.copy_(result, non_blocking=True)
code_lengths.copy_(entry.code_lengths[:count], non_blocking=True)
copied.record(torch.cuda.current_stream(self._device))
copied.synchronize()
# Returned codes must outlive this worker's next replay, including
# one-frame clips whose transpose is already contiguous.
return [
codes[:, row, : int(code_lengths[row])].transpose(0, 1).to(dtype=torch.long, copy=True).contiguous()
for row in range(count)
]
@@ -0,0 +1,334 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""One reference-audio encoder shared by the API processes of a server.
Each API process otherwise loads its own copy of the audio tokenizer on the
GPU and encodes only the clips it receives. When the processes share a
directory (``VLLM_OMNI_MOSS_REF_CODES_SHARED_DIR``), the first one to take the lock in it
hosts the encoder and serves the others over a Unix socket in that directory.
Requests from all processes are merged into batches, and only the host keeps
the tokenizer and its CUDA graphs on the GPU.
Messages carry raw arrays and a JSON header; nothing is unpickled. A client
that cannot reach the host encodes locally instead.
"""
from __future__ import annotations
import collections
import fcntl
import json
import os
import threading
import time
from collections.abc import Callable
from contextlib import nullcontext
from multiprocessing.connection import Client, Connection, Listener
import numpy as np
import torch
from vllm.logger import init_logger
logger = init_logger(__name__)
_LOCK_NAME = "ref-encoder.lock"
_SOCKET_NAME = "ref-encoder.sock"
# Clips one worker merges into an encode, across all API processes. Several
# workers encode concurrently on their own streams: one serial queue of large
# batches makes a burst of new references wait behind each other. A free
# worker takes what is queued without waiting for more: API processes already
# gather their clips before sending them.
_MAX_BATCH = 8
# A client gives up on the host after this long and encodes locally.
_CONNECT_TIMEOUT_S = 120.0
_REQUEST_TIMEOUT_S = 120.0
# First-time compilation can outlast a normal encode request. Readiness has a
# separate deadline and must finish before the API starts accepting requests.
_STARTUP_TIMEOUT_S = 1200.0
EncodeFn = Callable[[list[torch.Tensor]], list]
# Open for the life of the process: closing it would release the host lock.
_host_lock_handle = None
class SharedReferenceEncoderStartupError(RuntimeError):
"""The shared encoder cannot safely accept requests yet."""
def elect_host(shared_dir: str) -> bool:
"""True in the one process that holds the encoder lock for ``shared_dir``.
The lock is held for the life of the process, so a restarted server
elects a new host even if the old socket file is still present.
"""
global _host_lock_handle
path = os.path.join(shared_dir, _LOCK_NAME)
handle = open(path, "a+") # noqa: SIM115 - kept open to hold the lock
try:
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
handle.close()
return False
_host_lock_handle = handle
return True
def _send(conn: Connection, header: dict, arrays: list[np.ndarray]) -> None:
conn.send_bytes(json.dumps(header).encode())
for array in arrays:
conn.send_bytes(np.ascontiguousarray(array).tobytes())
def _recv(conn: Connection) -> tuple[dict, list[bytes]]:
header = json.loads(conn.recv_bytes().decode())
return header, [conn.recv_bytes() for _ in range(int(header["count"]))]
class _Job:
__slots__ = ("wav", "done", "result", "queued_at")
def __init__(self, wav: torch.Tensor) -> None:
self.wav = wav
self.done = threading.Event()
self.result: torch.Tensor | BaseException | None = None
self.queued_at = time.monotonic()
class SharedReferenceEncoderHost:
"""Serves reference encodes for every API process sharing ``shared_dir``.
``workers`` pairs each worker's encode function with an optional warmup
(for example, capturing that worker's CUDA graphs).
"""
def __init__(
self,
shared_dir: str,
workers: list[tuple[EncodeFn, Callable[[], object] | None]],
*,
stream_priority: int = 0,
batch_window_ms: float = 0.0,
) -> None:
self._stream_priority = stream_priority
self._batch_window_s = max(0.0, float(batch_window_ms)) / 1000
self._pending: collections.deque[_Job] = collections.deque()
# Includes jobs already taken by a worker, so shutdown can release
# every caller without waiting for an in-flight GPU operation.
self._outstanding: set[_Job] = set()
self._ready = threading.Condition()
self._path = os.path.join(shared_dir, _SOCKET_NAME)
if os.path.exists(self._path):
os.unlink(self._path) # left by a previous host; we hold the lock
self._listener = Listener(self._path, family="AF_UNIX")
os.chmod(self._path, 0o600)
self._closed = False
self._started = threading.Event()
self._startup_error: Exception | None = None
threading.Thread(
target=self._start_workers, args=(workers,), name="moss-ref-encoder-start", daemon=True
).start()
threading.Thread(target=self._accept_loop, name="moss-ref-encoder-accept", daemon=True).start()
logger.info("MOSS shared reference encoder serving at %s with %d workers", self._path, len(workers))
def encode(self, wavs: list[torch.Tensor]) -> list:
"""Encode on behalf of the host process itself."""
self.wait_until_ready()
jobs = [_Job(wav) for wav in wavs]
with self._ready:
if self._closed:
raise RuntimeError("shared reference encoder is closed")
self._outstanding.update(jobs)
self._pending.extend(jobs)
self._ready.notify(len(jobs))
for job in jobs:
job.done.wait()
return [job.result for job in jobs]
def close(self) -> None:
with self._ready:
self._closed = True
self._started.set()
for job in self._outstanding:
job.result = RuntimeError("shared reference encoder closed before encoding completed")
job.done.set()
self._outstanding.clear()
self._pending.clear()
self._ready.notify_all()
try:
self._listener.close()
except OSError:
pass
def wait_until_ready(self) -> None:
if not self._started.wait(_STARTUP_TIMEOUT_S):
raise SharedReferenceEncoderStartupError("shared reference encoder warmup timed out")
if self._closed or self._startup_error is not None:
raise SharedReferenceEncoderStartupError("shared reference encoder startup failed") from self._startup_error
def _accept_loop(self) -> None:
while not self._closed:
try:
conn = self._listener.accept()
except OSError:
return
threading.Thread(target=self._serve, args=(conn,), name="moss-ref-encoder-conn", daemon=True).start()
def _serve(self, conn: Connection) -> None:
with conn:
while True:
try:
header, payloads = _recv(conn)
except (EOFError, OSError):
return
if header.get("op") == "ready":
try:
self.wait_until_ready()
error = None
except SharedReferenceEncoderStartupError as exc:
error = str(exc)
try:
_send(conn, {"count": 0, "ready": error is None, "error": error}, [])
except OSError:
return
continue
wavs = [
torch.from_numpy(np.frombuffer(data, dtype=np.float32).reshape(shape).copy())
for data, shape in zip(payloads, header["shapes"])
]
results = self.encode(wavs)
errors = [None if isinstance(r, torch.Tensor) else repr(r) for r in results]
codes = [r.to(torch.int32).numpy() for r in results if isinstance(r, torch.Tensor)]
reply = {"count": len(codes), "shapes": [list(c.shape) for c in codes], "errors": errors}
try:
_send(conn, reply, codes)
except OSError:
return
def _start_workers(self, workers: list[tuple[EncodeFn, Callable[[], object] | None]]) -> None:
# Every warmup (graph capture) finishes before any worker encodes: CUDA
# work in another thread would invalidate a capture in progress.
try:
for _, warmup in workers:
if warmup is not None:
warmup()
if self._closed:
return
for index, (encode_fn, _) in enumerate(workers):
threading.Thread(
target=self._encode_loop, args=(encode_fn,), name=f"moss-ref-encoder-{index}", daemon=True
).start()
logger.info("MOSS shared reference encoder ready: %d workers", len(workers))
except Exception as exc: # noqa: BLE001 - wake readiness waiters with the failure
self._startup_error = exc
logger.exception("MOSS shared reference encoder startup failed")
finally:
self._started.set()
def _encode_loop(self, encode_fn: EncodeFn) -> None:
stream = torch.cuda.Stream(priority=self._stream_priority) if torch.cuda.is_available() else None
with torch.cuda.stream(stream) if stream is not None else nullcontext():
self._serve_jobs(encode_fn)
def _serve_jobs(self, encode_fn: EncodeFn) -> None:
while True:
with self._ready:
while True:
if self._closed:
return
if not self._pending:
self._ready.wait()
continue
remaining = self._pending[0].queued_at + self._batch_window_s - time.monotonic()
if remaining > 0 and len(self._pending) < _MAX_BATCH:
# Keep clips in the common queue while collecting;
# workers must not each reserve a singleton first.
self._ready.wait(remaining)
continue
break
jobs = [self._pending.popleft() for _ in range(min(_MAX_BATCH, len(self._pending)))]
try:
results = encode_fn([job.wav for job in jobs])
if len(results) != len(jobs):
raise RuntimeError(
f"shared reference encoder returned {len(results)} results for {len(jobs)} clips"
)
except Exception as error: # noqa: BLE001 - every waiter gets the failure
results = [error] * len(jobs)
with self._ready:
for job, result in zip(jobs, results, strict=True):
# close() may already have completed this caller with an
# error. A late GPU result must not replace that outcome.
if not job.done.is_set():
job.result = result
job.done.set()
self._outstanding.discard(job)
class SharedReferenceEncoderClient:
"""Sends reference clips to the host process of ``shared_dir``."""
def __init__(self, shared_dir: str) -> None:
self._path = os.path.join(shared_dir, _SOCKET_NAME)
# One connection per request in flight; idle ones are reused.
self._idle: list[Connection] = []
self._lock = threading.Lock()
self._connected = False
def _connect(self) -> Connection:
# The host may still be starting until the first connection succeeds;
# later reconnects are quick.
deadline = time.monotonic() + (1.0 if self._connected else _CONNECT_TIMEOUT_S)
while True:
try:
conn = Client(self._path, family="AF_UNIX")
self._connected = True
break
except (FileNotFoundError, ConnectionRefusedError):
if time.monotonic() >= deadline:
raise
time.sleep(0.2)
try:
_send(conn, {"op": "ready", "count": 0}, [])
if not conn.poll(_STARTUP_TIMEOUT_S):
raise SharedReferenceEncoderStartupError("shared reference encoder readiness timed out")
header, _ = _recv(conn)
if not header.get("ready"):
raise SharedReferenceEncoderStartupError(header.get("error", "shared reference encoder not ready"))
return conn
except BaseException:
conn.close()
raise
def wait_until_ready(self) -> None:
"""Startup handshake, without encoding or populating the reference cache."""
try:
conn = self._connect()
except Exception as exc:
raise SharedReferenceEncoderStartupError("shared reference encoder is not ready") from exc
with self._lock:
self._idle.append(conn)
def encode(self, wavs: list[torch.Tensor]) -> list:
"""Codes (or an exception) per clip; raises when the host is unreachable."""
with self._lock:
conn = self._idle.pop() if self._idle else None
try:
if conn is None:
conn = self._connect()
arrays = [w.detach().to(torch.float32).cpu().numpy() for w in wavs]
_send(conn, {"count": len(arrays), "shapes": [list(a.shape) for a in arrays]}, arrays)
if not conn.poll(_REQUEST_TIMEOUT_S):
raise TimeoutError(f"no reply from the shared reference encoder in {_REQUEST_TIMEOUT_S}s")
header, payloads = _recv(conn)
except BaseException:
if conn is not None:
conn.close()
raise
with self._lock:
self._idle.append(conn)
codes = iter(
torch.from_numpy(np.frombuffer(data, dtype=np.int32).reshape(shape).copy()).long()
for data, shape in zip(payloads, header["shapes"])
)
return [next(codes) if error is None else RuntimeError(error) for error in header["errors"]]
@@ -0,0 +1,475 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Direct-slot ring attention for MOSS streaming codec.
Inputs q/k/v are (execution_batch, heads, padded_T, head_dim). cache is
contiguous (2, state_capacity, heads, ring_capacity, head_dim). end_offset
contains absolute *pre-call* positions per state slot. slot_ids and valid_lengths
contain one entry per execution row. valid_lengths=0 is a scratch/padding row.
This op writes only valid tokens, attends to the resulting ring with the same
position semantics as RingKVCache.complete, then advances only active offsets.
Slots must be unique and in bounds; 0 <= valid_lengths <= padded_T. Chunks
longer than the ring retain only their final ring_capacity tokens.
These device-value preconditions are caller-owned to avoid host synchronization.
The op deliberately preserves chunk-complete ring overwrite semantics; it does
not recover entries overwritten by later tokens of the same input chunk.
"""
import torch
from vllm.triton_utils import tl, triton
@triton.jit
def _write(
k_ptr,
v_ptr,
cache_ptr,
end_ptr,
slots_ptr,
lengths_ptr,
ks0: tl.constexpr,
ks1: tl.constexpr,
ks2: tl.constexpr,
ks3: tl.constexpr,
vs0: tl.constexpr,
vs1: tl.constexpr,
vs2: tl.constexpr,
vs3: tl.constexpr,
end_stride: tl.constexpr,
slot_stride: tl.constexpr,
length_stride: tl.constexpr,
state_capacity: tl.constexpr,
num_heads: tl.constexpr,
num_frames: tl.constexpr,
ring_capacity: tl.constexpr,
head_dim: tl.constexpr,
block_size: tl.constexpr,
):
row = tl.program_id(0)
slot = tl.load(slots_ptr + row * slot_stride)
length = tl.load(lengths_ptr + row * length_stride)
start = tl.load(end_ptr + slot * end_stride)
x = tl.program_id(1) * block_size + tl.arange(0, block_size)
dim = x % head_dim
token = (x // head_dim) % num_frames
head = x // (num_frames * head_dim)
# For chunks longer than the ring, write only the final C tokens. This
# avoids duplicate physical addresses (and write races) in one launch.
skip = tl.maximum(length - ring_capacity, 0)
source_token = token + skip
mask = (head < num_heads) & (source_token < length)
pos = (start + source_token) % ring_capacity
dst = ((slot * num_heads + head) * ring_capacity + pos) * head_dim + dim
kval = tl.load(k_ptr + row * ks0 + head * ks1 + source_token * ks2 + dim * ks3, mask, 0)
vval = tl.load(v_ptr + row * vs0 + head * vs1 + source_token * vs2 + dim * vs3, mask, 0)
tl.store(cache_ptr + dst, kval, mask)
tl.store(cache_ptr + state_capacity * num_heads * ring_capacity * head_dim + dst, vval, mask)
@triton.jit
def _attend(
q_ptr,
cache_ptr,
end_ptr,
slots_ptr,
lengths_ptr,
out_ptr,
qs0: tl.constexpr,
qs1: tl.constexpr,
qs2: tl.constexpr,
qs3: tl.constexpr,
end_stride: tl.constexpr,
slot_stride: tl.constexpr,
length_stride: tl.constexpr,
state_capacity: tl.constexpr,
num_heads: tl.constexpr,
num_frames: tl.constexpr,
ring_capacity: tl.constexpr,
head_dim: tl.constexpr,
context_size: tl.constexpr,
block_m: tl.constexpr,
block_n: tl.constexpr,
skip_empty_tiles: tl.constexpr = False,
):
rows = tl.program_id(0) * block_m + tl.arange(0, block_m)
bh = tl.program_id(1)
batch, head = bh // num_heads, bh % num_heads
slot = tl.load(slots_ptr + batch * slot_stride)
length = tl.load(lengths_ptr + batch * length_stride)
start = tl.load(end_ptr + slot * end_stride)
end = start + length
dims = tl.arange(0, head_dim)
q = tl.load(
q_ptr + batch * qs0 + head * qs1 + rows[:, None] * qs2 + dims[None, :] * qs3, rows[:, None] < num_frames, 0
)
acc = tl.full((block_m, head_dim), 0.0, tl.float32)
maximum = tl.full((block_m,), -float("inf"), tl.float32)
denominator = tl.full((block_m,), 0.0, tl.float32)
# Padding rows own scratch slots and must emit zeros without reading KV.
# Keep valid-row accumulation and the ring overwrite semantics unchanged.
if skip_empty_tiles:
block_start = tl.program_id(0) * block_m
live = (length > 0) & (block_start < length)
live = live & (block_start + block_m > tl.maximum(length - ring_capacity, 0))
else:
live = length > 0
if live:
# Physical-ring order matches the existing masked_attention accumulation.
last = end - 1
last_index = last % ring_capacity
if skip_empty_tiles:
# Before the first wrap, only physical keys [0, end) exist.
# Omitted blocks have zero probability for every query; keep the
# physical order and arithmetic of all remaining blocks unchanged.
key_blocks = tl.cdiv(tl.minimum(end, ring_capacity), block_n)
else:
key_blocks = tl.cdiv(ring_capacity, block_n)
for first in range(key_blocks):
cols = first * block_n + tl.arange(0, block_n)
delta = cols - last_index
positions = tl.where(delta <= 0, last + delta, last + delta - ring_capacity)
distance = start + rows[:, None] - positions[None, :]
allowed = (
(rows[:, None] < length)
& (cols[None, :] < ring_capacity)
& (positions[None, :] >= 0)
& (positions[None, :] < end)
& (distance >= 0)
)
if context_size > 0:
allowed = allowed & (distance < context_size)
base = (slot * num_heads + head) * ring_capacity * head_dim
k = tl.load(cache_ptr + base + cols[None, :] * head_dim + dims[:, None], cols[None, :] < ring_capacity, 0)
score = tl.dot(q, k).to(tl.float32) * (head_dim**-0.5)
score = tl.where(allowed, score, -float("inf"))
next_max = tl.maximum(maximum, tl.max(score, 1))
safe_max = tl.where(next_max == -float("inf"), 0.0, next_max)
rescale = tl.exp(maximum - safe_max)
p = tl.exp(score - safe_max[:, None])
denominator = denominator * rescale + tl.sum(p, 1)
v = tl.load(
cache_ptr
+ state_capacity * num_heads * ring_capacity * head_dim
+ base
+ cols[:, None] * head_dim
+ dims[None, :],
cols[:, None] < ring_capacity,
0,
)
acc = acc * rescale[:, None] + tl.dot(p.to(v.dtype), v)
maximum = next_max
out = acc / tl.where(denominator > 0, denominator, 1.0)[:, None]
tl.store(
out_ptr + ((batch * num_heads + head) * num_frames + rows[:, None]) * head_dim + dims[None, :],
out,
rows[:, None] < num_frames,
)
@triton.jit
def _advance(
end_ptr,
slots_ptr,
lengths_ptr,
end_stride: tl.constexpr,
slot_stride: tl.constexpr,
length_stride: tl.constexpr,
batch_size: tl.constexpr,
block_size: tl.constexpr,
):
row = tl.arange(0, block_size)
slot = tl.load(slots_ptr + row * slot_stride, row < batch_size, 0)
length = tl.load(lengths_ptr + row * length_stride, row < batch_size, 0)
active = (row < batch_size) & (length > 0)
start = tl.load(end_ptr + slot * end_stride, active, 0)
tl.store(end_ptr + slot * end_stride, start + length, active)
@triton.jit
def _write_attend(
q_ptr,
k_ptr,
v_ptr,
cache_ptr,
starts_ptr,
slots_ptr,
lengths_ptr,
out_ptr,
qs0: tl.constexpr,
qs1: tl.constexpr,
qs2: tl.constexpr,
qs3: tl.constexpr,
ks0: tl.constexpr,
ks1: tl.constexpr,
ks2: tl.constexpr,
ks3: tl.constexpr,
vs0: tl.constexpr,
vs1: tl.constexpr,
vs2: tl.constexpr,
vs3: tl.constexpr,
start_stride: tl.constexpr,
slot_stride: tl.constexpr,
length_stride: tl.constexpr,
state_capacity: tl.constexpr,
num_heads: tl.constexpr,
num_frames: tl.constexpr,
ring_capacity: tl.constexpr,
head_dim: tl.constexpr,
context_size: tl.constexpr,
write_frames: tl.constexpr,
block_m: tl.constexpr,
block_n: tl.constexpr,
block_w: tl.constexpr,
):
rows = tl.program_id(0) * block_m + tl.arange(0, block_m)
bh = tl.program_id(1)
batch, head = bh // num_heads, bh % num_heads
slot = tl.load(slots_ptr + batch * slot_stride)
length = tl.load(lengths_ptr + batch * length_stride)
start = tl.load(starts_ptr + batch * start_stride)
end = start + length
dims = tl.arange(0, head_dim)
base = (slot * num_heads + head) * ring_capacity * head_dim
v_base = state_capacity * num_heads * ring_capacity * head_dim + base
# Every program of this (row, head) writes the whole chunk for its head,
# so it only reads ring entries it wrote itself (duplicate writes across
# query blocks store identical values). Chunks longer than the ring keep
# only their final ring_capacity tokens, as in _write.
skip = tl.maximum(length - ring_capacity, 0)
for first in range(tl.cdiv(write_frames, block_w)):
token = first * block_w + tl.arange(0, block_w)
source = token + skip
mask = (source < length)[:, None]
pos = (start + source) % ring_capacity
kval = tl.load(k_ptr + batch * ks0 + head * ks1 + source[:, None] * ks2 + dims[None, :] * ks3, mask, 0)
vval = tl.load(v_ptr + batch * vs0 + head * vs1 + source[:, None] * vs2 + dims[None, :] * vs3, mask, 0)
dst = pos[:, None] * head_dim + dims[None, :]
tl.store(cache_ptr + base + dst, kval, mask)
tl.store(cache_ptr + v_base + dst, vval, mask)
tl.debug_barrier()
q = tl.load(
q_ptr + batch * qs0 + head * qs1 + rows[:, None] * qs2 + dims[None, :] * qs3, rows[:, None] < num_frames, 0
)
acc = tl.full((block_m, head_dim), 0.0, tl.float32)
maximum = tl.full((block_m,), -float("inf"), tl.float32)
denominator = tl.full((block_m,), 0.0, tl.float32)
if length > 0:
last = end - 1
last_index = last % ring_capacity
for first in range(tl.cdiv(ring_capacity, block_n)):
cols = first * block_n + tl.arange(0, block_n)
delta = cols - last_index
positions = tl.where(delta <= 0, last + delta, last + delta - ring_capacity)
distance = start + rows[:, None] - positions[None, :]
allowed = (
(rows[:, None] < length)
& (cols[None, :] < ring_capacity)
& (positions[None, :] >= 0)
& (positions[None, :] < end)
& (distance >= 0)
)
if context_size > 0:
allowed = allowed & (distance < context_size)
k = tl.load(cache_ptr + base + cols[None, :] * head_dim + dims[:, None], cols[None, :] < ring_capacity, 0)
score = tl.dot(q, k).to(tl.float32) * (head_dim**-0.5)
score = tl.where(allowed, score, -float("inf"))
next_max = tl.maximum(maximum, tl.max(score, 1))
safe_max = tl.where(next_max == -float("inf"), 0.0, next_max)
rescale = tl.exp(maximum - safe_max)
p = tl.exp(score - safe_max[:, None])
denominator = denominator * rescale + tl.sum(p, 1)
v = tl.load(
cache_ptr + v_base + cols[:, None] * head_dim + dims[None, :],
cols[:, None] < ring_capacity,
0,
)
acc = acc * rescale[:, None] + tl.dot(p.to(v.dtype), v)
maximum = next_max
out = acc / tl.where(denominator > 0, denominator, 1.0)[:, None]
tl.store(
out_ptr + ((batch * num_heads + head) * num_frames + rows[:, None]) * head_dim + dims[None, :],
out,
rows[:, None] < num_frames,
)
@torch.library.custom_op("vllm_omni::moss_slot_ring_attention", mutates_args=("cache", "end_offset"))
def slot_ring_attention(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cache: torch.Tensor,
end_offset: torch.Tensor,
slot_ids: torch.Tensor,
valid_lengths: torch.Tensor,
context: int = -1,
skip_empty_tiles: bool = False,
) -> torch.Tensor:
"""Fused address/mask path, with three ordered launches and no KV gather.
q/k are already rotary-position encoded. The caller still advances its
separate MHA/RoPE offset state; this op advances only end_offset in place.
context=-1 means unbounded causal attention over available ring entries.
Outputs beyond each row's valid_lengths are exactly zero.
skip_empty_tiles skips wholly masked tiles for small (T <= 32) chunks.
Larger chunks keep the static loop, which benchmarks faster after wrap.
"""
b, h, t, d = q.shape
if k.shape != q.shape or v.shape != q.shape:
raise ValueError("q/k/v must have identical shapes")
if q.dtype not in (torch.bfloat16, torch.float16) or k.dtype != q.dtype or v.dtype != q.dtype:
raise ValueError("q/k/v must share bf16 or fp16 dtype")
if d not in (32, 64, 128) or t < 1:
raise ValueError("requires head_dim 32/64/128 and positive padded_T")
if cache.ndim != 5 or cache.shape[0] != 2 or cache.shape[2] != h or cache.shape[4] != d:
raise ValueError("cache must be (2, state_capacity, heads, capacity, head_dim)")
s, c = cache.shape[1], cache.shape[3]
if not cache.is_contiguous() or cache.dtype != q.dtype:
raise ValueError("cache must be contiguous and share q dtype")
if end_offset.shape != (s,) or slot_ids.shape != (b,) or valid_lengths.shape != (b,):
raise ValueError("invalid metadata shapes")
if any(x.dtype not in (torch.int32, torch.int64) for x in (end_offset, slot_ids, valid_lengths)):
raise ValueError("metadata must be int32/int64")
if q.device.type != "cuda" or any(x.device != q.device for x in (k, v, cache, end_offset, slot_ids, valid_lengths)):
raise ValueError("all tensors must share one CUDA device")
if context == 0 or context < -1:
raise ValueError("context must be -1 or positive")
out = torch.empty(q.shape, device=q.device, dtype=q.dtype)
if b == 0:
return out
strides = (end_offset.stride(0), slot_ids.stride(0), valid_lengths.stride(0))
_write[(b, triton.cdiv(h * t * d, 256))](
k, v, cache, end_offset, slot_ids, valid_lengths, *k.stride(), *v.stride(), *strides, s, h, t, c, d, 256
)
bm = 16 if t <= 32 else 64
_attend[(triton.cdiv(t, bm), b * h)](
q,
cache,
end_offset,
slot_ids,
valid_lengths,
out,
*q.stride(),
*strides,
s,
h,
t,
c,
d,
context,
bm,
64,
skip_empty_tiles=skip_empty_tiles and t <= 32,
num_warps=8 if c <= 256 else 4,
num_stages=2,
)
_advance[(1,)](end_offset, slot_ids, valid_lengths, *strides, b, triton.next_power_of_2(b))
return out
@slot_ring_attention.register_fake
def _(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cache: torch.Tensor,
end_offset: torch.Tensor,
slot_ids: torch.Tensor,
valid_lengths: torch.Tensor,
context: int = -1,
skip_empty_tiles: bool = False,
) -> torch.Tensor:
return torch.empty(q.shape, device=q.device, dtype=q.dtype)
def _check_inputs(q, k, v, cache, metadata, context):
b, h, t, d = q.shape
if k.shape != q.shape or v.shape != q.shape:
raise ValueError("q/k/v must have identical shapes")
if q.dtype not in (torch.bfloat16, torch.float16) or k.dtype != q.dtype or v.dtype != q.dtype:
raise ValueError("q/k/v must share bf16 or fp16 dtype")
if d not in (32, 64, 128) or t < 1:
raise ValueError("requires head_dim 32/64/128 and positive padded_T")
if cache.ndim != 5 or cache.shape[0] != 2 or cache.shape[2] != h or cache.shape[4] != d:
raise ValueError("cache must be (2, state_capacity, heads, capacity, head_dim)")
if not cache.is_contiguous() or cache.dtype != q.dtype:
raise ValueError("cache must be contiguous and share q dtype")
if any(x.dtype not in (torch.int32, torch.int64) for x in metadata):
raise ValueError("metadata must be int32/int64")
if q.device.type != "cuda" or any(x.device != q.device for x in (k, v, cache, *metadata)):
raise ValueError("all tensors must share one CUDA device")
if context == 0 or context < -1:
raise ValueError("context must be -1 or positive")
@torch.library.custom_op("vllm_omni::moss_slot_ring_attention_rows", mutates_args=("cache",))
def slot_ring_attention_rows(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cache: torch.Tensor,
starts: torch.Tensor,
slot_ids: torch.Tensor,
valid_lengths: torch.Tensor,
context: int = -1,
) -> torch.Tensor:
"""``slot_ring_attention`` as one launch, with per-row start positions.
``starts`` holds each execution row's pre-call absolute position (the
caller's gathered offset). The ring is written and attended exactly as by
``slot_ring_attention``; no offset is advanced here, so the caller owns
every offset update and can fuse them with its own.
"""
b, h, t, d = q.shape
_check_inputs(q, k, v, cache, (starts, slot_ids, valid_lengths), context)
if starts.shape != (b,) or slot_ids.shape != (b,) or valid_lengths.shape != (b,):
raise ValueError("invalid metadata shapes")
out = torch.empty(q.shape, device=q.device, dtype=q.dtype)
if b == 0:
return out
s, c = cache.shape[1], cache.shape[3]
bm = 16 if t <= 32 else 64
_write_attend[(triton.cdiv(t, bm), b * h)](
q,
k,
v,
cache,
starts,
slot_ids,
valid_lengths,
out,
*q.stride(),
*k.stride(),
*v.stride(),
starts.stride(0),
slot_ids.stride(0),
valid_lengths.stride(0),
s,
h,
t,
c,
d,
context,
min(t, c),
bm,
64,
16,
num_warps=8 if c <= 256 else 4,
num_stages=2,
)
return out
@slot_ring_attention_rows.register_fake
def _(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cache: torch.Tensor,
starts: torch.Tensor,
slot_ids: torch.Tensor,
valid_lengths: torch.Tensor,
context: int = -1,
) -> torch.Tensor:
return torch.empty(q.shape, device=q.device, dtype=q.dtype)
@@ -0,0 +1,99 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""First codec frame -> PCM inside the Qwen3-Omni Talker process.
Code2Wav decodes each streaming chunk statelessly from its codes and left
context. With a one-frame first chunk (``codec_chunk_ramp`` starting at 1),
chunk 0 is a context-free decode of the stream's first frame, which the Talker
can run as soon as eager MTP completes that frame, removing the connector hop
and the Code2Wav step from time to first audio. The orchestrator then trims
Code2Wav's copy of those samples.
The decoder is a second ``Qwen3OmniMoeCode2Wav`` built and loaded exactly like
the Code2Wav stage's, and captured with the same one-frame input shape as that
stage's ``(batch=1, frames=1)`` graph. Optional cuDNN autotuning can select
different algorithms in the two processes and change floating-point rounding.
Each bucket gets its own graph pool because it replays on a side stream while
the Talker's own graphs may still run.
"""
from __future__ import annotations
import os
from collections.abc import Iterable, Sequence
from typing import Any
import torch
import torch.nn as nn
from torch.cuda import CUDAGraph
from vllm.logger import init_logger
from vllm_omni.model_executor.models.common.qwen3_code_predictor import CodePredictorWrapper
from vllm_omni.model_executor.models.common.talker_first_audio import supports_talker_first_audio
from vllm_omni.model_executor.stage_input_processors.chunk_size_utils import parse_chunk_ramp
logger = init_logger(__name__)
def talker_first_audio_enabled(vllm_config: Any) -> bool:
"""Opt in only when chunk 0 carries the first frame in the same step."""
extra = CodePredictorWrapper._stage_connector_extra_config(vllm_config)
ramp = parse_chunk_ramp(extra)
initial_frames = ramp[0] if ramp is not None else int(extra.get("initial_codec_chunk_frames") or 0)
return (
CodePredictorWrapper._parse_bool_config(extra.get("talker_first_audio"))
and initial_frames == 1
and os.environ.get("VLLM_OMNI_TALKER_FIRST_AUDIO", "1") == "1"
and supports_talker_first_audio(vllm_config)
)
class Qwen3OmniFirstFrameDecoder(nn.Module):
def __init__(self, code2wav: nn.Module, sample_rate: int) -> None:
super().__init__()
self.code2wav = code2wav
self.num_quantizers = int(code2wav.config.num_quantizers)
self.sample_rate = int(sample_rate)
self._graphs: dict[int, tuple[CUDAGraph, torch.Tensor, torch.Tensor]] = {}
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load the checkpoint's ``code2wav.*`` weights as the Code2Wav stage does."""
loaded = self.code2wav.load_weights(weights)
if hasattr(self.code2wav, "precompute_snake_caches"):
self.code2wav.precompute_snake_caches()
return loaded
@torch.inference_mode()
def capture(self, batch_sizes: Sequence[int] = (1, 2, 4, 8)) -> None:
device = next(self.code2wav.parameters()).device
if device.type != "cuda":
return
for batch_size in sorted(set(batch_sizes)):
static_input = torch.zeros(batch_size, self.num_quantizers, 1, dtype=torch.long, device=device)
self.code2wav(static_input)
torch.accelerator.synchronize(device)
graph = CUDAGraph()
with torch.cuda.graph(graph, pool=torch.cuda.graph_pool_handle()):
static_output = self.code2wav(static_input)
self._graphs[batch_size] = (graph, static_input, static_output)
logger.info("Captured Qwen3-Omni Talker first-frame decoder graphs for batch sizes %s", sorted(self._graphs))
@torch.inference_mode()
def decode(self, codes: torch.Tensor) -> torch.Tensor:
"""``codes`` [n, num_quantizers] long on device -> float32 PCM [n, samples] (new tensor)."""
if not self._graphs:
return self.code2wav(codes.to(torch.long).unsqueeze(-1))[:, 0, :].float()
outputs = []
largest = max(self._graphs)
for start in range(0, int(codes.shape[0]), largest):
chunk = codes[start : start + largest]
rows = int(chunk.shape[0])
batch_size = min(size for size in self._graphs if size >= rows)
graph, static_input, static_output = self._graphs[batch_size]
static_input.zero_()
static_input[:rows, :, 0].copy_(chunk)
graph.replay()
# A replay overwrites graph-owned storage; .float() aliases FP32 outputs.
outputs.append(static_output[:rows, 0, :].to(dtype=torch.float32, copy=True))
return torch.cat(outputs, dim=0)
@@ -45,6 +45,8 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
engine_output_type="latent",
custom_process_next_stage_input_func=(f"{_PROC}.thinker2talker_full_payload"),
async_chunk_process_next_stage_input_func=(f"{_PROC}.thinker2talker_async_chunk"),
# Takes effect only when the deploy selects model_runner v2 for this stage.
supports_native_mrv2_data_plane=True,
sampling_constraints={"detokenize": True},
),
StagePipelineConfig(
@@ -57,6 +59,8 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
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"),
# Each stage can use MRv2 when its deploy selects model_runner v2.
supports_native_mrv2_data_plane=True,
sampling_constraints={
"detokenize": False,
"stop_token_ids": [2150],
@@ -72,6 +76,7 @@ QWEN3_OMNI_PIPELINE = PipelineConfig(
final_output_type="audio",
hf_config_name="thinker_config",
engine_output_type="audio",
supports_native_mrv2_data_plane=True,
sampling_constraints={"detokenize": True},
requires_full_payload_input=True,
),
@@ -4,7 +4,7 @@
"""Inference-only Qwen3-Omni-Moe unified model (thinker + talker + code2wav)."""
import asyncio
from collections.abc import AsyncGenerator, Iterable
from collections.abc import AsyncGenerator, Iterable, Mapping
from functools import cached_property
from typing import Any
@@ -50,6 +50,7 @@ from vllm_omni.data_entry_keys import Embeddings, HiddenStates, Ids, OmniPayload
from vllm_omni.metrics import definitions as defs
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.model_executor.models.qwen3_omni.first_frame_decoder import talker_first_audio_enabled
from vllm_omni.model_executor.models.qwen3_omni.quantization import (
apply_outer_quant_config_mapping,
)
@@ -61,7 +62,9 @@ from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker import (
Qwen3OmniMoeThinkerProcessingInfo,
)
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights, safe_tensor_reshape
from vllm_omni.model_executor.stage_input_processors.chunk_size_utils import parse_chunk_ramp, ramp_decode_windows
from vllm_omni.platforms import current_omni_platform
from vllm_omni.utils.device_copy import to_device_nonblocking
# Special token IDs for Qwen3 Omni MoE
# Reference: https://huggingface.co/Qwen/Qwen3-Omni-30B-A3B-Instruct/blob/main/tokenizer_config.json
@@ -88,6 +91,13 @@ TALKER_CODEC_THINK_EOS_ID = 4205 # Think mode end
logger = init_logger(__name__)
def _cpu_long(values: Any) -> torch.Tensor:
"""Token ids as a host tensor (payload ids arrive as lists or CPU tensors)."""
if isinstance(values, torch.Tensor):
return values.detach().to("cpu", torch.long)
return torch.as_tensor(values, dtype=torch.long)
@MULTIMODAL_REGISTRY.register_processor(
Qwen3OmniMoeThinkerMultiModalProcessor,
info=Qwen3OmniMoeThinkerProcessingInfo,
@@ -135,6 +145,11 @@ class Qwen3OmniMoeForConditionalGeneration(
realtime_max_tokens = 64
# Set by the MRv2 model state once eager Talker-MTP frames are enabled.
eager_frames_active = False
# Postprocess only stores the last hidden for the deferred decode MTP.
eager_frames_need_postprocess = False
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
self.have_multimodal_outputs = True
@@ -180,9 +195,18 @@ class Qwen3OmniMoeForConditionalGeneration(
# thinker layers and the forward must return what stock vLLM expects.
self.is_staged_run = getattr(vllm_config.model_config, "model_stage", None) is not None
self._returns_tuple = self.model_stage == "thinker" and self.is_staged_run
# The captured layers are a fixed-structure tensor dict on the token
# axis, so MRv2 may replay decode in FULL graphs and return them too.
self.supports_mrv2_full_graph_aux_outputs = self._returns_tuple
if self.model_stage == "thinker":
self.use_async_omni_output = True
# MRv2: the Talker's prompt ends with the first generated token's
# embedding; publishing it with the prefill output lets the first
# Thinker->Talker chunk leave one decode step earlier.
# Speculative sampling can change the token width between steps.
# Keep the existing capture-stream handoff for the entire request.
self.publishes_sampled_embeddings = self.is_staged_run and vllm_config.speculative_config is None
# Initialize thinker model (multimodal processing + text generation)
# Create a new vllm_config with thinker_config as the hf_config
thinker_vllm_config = vllm_config.with_hf_config(
@@ -233,6 +257,19 @@ class Qwen3OmniMoeForConditionalGeneration(
)
self.model = self.talker
self.code2wav = None
# Residual codebooks (MTP) output key, shared by the V1 runner
# (talker_mtp_*) and the MRv2 model state (mtp_*).
self.talker_mtp_output_key = ("codes", "audio")
# MRv2: the step that samples CB0 may complete the frame itself,
# since the residual codebooks depend only on CB0 and that step's
# last hidden, and the next input is their embedding sum plus the
# next text step (see ``talker_mtp``).
self.mtp_eager_frames = True
self._codec_codebook_size = int(talker_config.code_predictor_config.vocab_size)
self._apply_subtalker_sampling_params(getattr(vllm_config.model_config, "subtalker_sampling_params", None))
# Decode each stream's first frame here (see talker_first_audio_enabled).
self.first_frame_decoder = self._build_first_frame_decoder(vllm_config, code2wav_config, prefix)
# for CI: Initialize special tokens embeddings early to avoid AttributeError when loading dummy weights
self._init_special_tokens_embeddings()
@@ -247,6 +284,9 @@ class Qwen3OmniMoeForConditionalGeneration(
# talker MTP codec codes must stay on GPU to avoid a per-step D2H
# sync stall; build_mm_cpu handles the eventual D2H at payload time.
("codes", "audio"),
# The growing thinker-embed cache is read and extended every
# decode step; on CPU it cost a blocking D2H per request per step.
("embed", "cached_decode"),
}
elif self.model_stage == "code2wav":
@@ -285,6 +325,74 @@ class Qwen3OmniMoeForConditionalGeneration(
["hidden_states", "residual", *incoming_captures], thinker_config.text_config.hidden_size
)
@staticmethod
def _build_first_frame_decoder(
vllm_config: VllmConfig,
code2wav_config: Qwen3OmniMoeCode2WavConfig,
prefix: str,
) -> nn.Module | None:
"""A Code2Wav copy for the Talker's first-frame audio, or None when disabled.
Only a streaming (async-chunk) Talker hands frames to Code2Wav chunk by
chunk, so only there is a stream's first chunk a context-free decode.
The copy is built like the Code2Wav stage's module (same class, config,
dtype and device context). Optional cuDNN autotuning is also shared,
although per-process algorithm selection can change PCM rounding.
"""
if not talker_first_audio_enabled(vllm_config):
return None
from vllm_omni.model_executor.models.qwen3_omni.first_frame_decoder import Qwen3OmniFirstFrameDecoder
code2wav = init_vllm_registered_model(
vllm_config=vllm_config.with_hf_config(code2wav_config, architectures=["Qwen3OmniMoeCode2Wav"]),
prefix=maybe_prefix(prefix, "first_frame_decoder.code2wav"),
hf_config=code2wav_config,
architectures=["Qwen3OmniMoeCode2Wav"],
)
logger.info("Qwen3-Omni Talker decodes first-frame audio in-stage")
return Qwen3OmniFirstFrameDecoder(code2wav, defs.resolve_audio_sample_rate(code2wav_config))
def capture_first_frame_graphs(self) -> None:
"""Called by the MRv2 runner after its own graph capture."""
decoder = getattr(self, "first_frame_decoder", None)
if decoder is not None:
decoder.capture()
def _apply_subtalker_sampling_params(self, params: Any) -> None:
"""Apply the deploy's ``subtalker_sampling_params`` to the residual-codebook predictor.
The predictor samples in its "stored" mode (top-k 50, top-p 0.8 by
default) and ignores per-call sampling arguments; ``do_sample: false``
makes it greedy.
"""
if not isinstance(params, Mapping) or not params:
return
predictor = self.talker.code_predictor
predictor.set_sampling_params(
top_k=int(params.get("top_k", predictor._top_k)),
top_p=float(params.get("top_p", predictor._top_p)),
do_sample=bool(params.get("do_sample", True)),
)
# MRv2 capability names read the V1 canonical values, so a platform patch
# of ``talker_mtp_graph_safe`` applies to both runners.
@property
def mtp(self):
"""MRv2 name for ``talker_mtp``; only the Talker stage has an MTP."""
return self.talker_mtp if self.model_stage == "talker" else None
@property
def mtp_output_key(self) -> tuple[str, str]:
return getattr(self, "talker_mtp_output_key", ("codes", "audio"))
@property
def mtp_graph_safe(self) -> bool:
return bool(getattr(self, "talker_mtp_graph_safe", False))
def mtp_frame_valid(self, layer0: torch.Tensor) -> torch.Tensor:
"""Rows whose CB0 is a codec id rather than codec EOS (or another special id)."""
return (layer0 >= 0) & (layer0 < self._codec_codebook_size)
def _thinker_capture_layer_indices(self) -> list[int]:
accept_layer = getattr(self.talker_config, "accept_hidden_layer", None)
return [0, int(accept_layer)] if self.is_staged_run and accept_layer is not None else []
@@ -496,38 +604,47 @@ class Qwen3OmniMoeForConditionalGeneration(
# ========== Stage 3: Code2Wav ==========
elif self.model_stage == "code2wav":
seq_token_counts: list[int] | None = kwargs.get("seq_token_counts")
num_quantizers = int(self.code2wav.config.num_quantizers)
# Extract codec codes from input
if input_ids.shape[0] % 16 == 0:
if input_ids.shape[0] % num_quantizers == 0:
if seq_token_counts is not None:
max_seq_len = max(seq_token_counts) // 16
max_seq_len = max(seq_token_counts) // num_quantizers
batch_size = len(seq_token_counts)
split_codes = torch.split(input_ids, seq_token_counts, dim=0)
codes = torch.zeros((batch_size, 16, max_seq_len), device=input_ids.device, dtype=input_ids.dtype)
codes = torch.zeros(
(batch_size, num_quantizers, max_seq_len), device=input_ids.device, dtype=input_ids.dtype
)
for idx, code in enumerate(split_codes):
seq_len = code.shape[0] // 16
codes[idx, :, :seq_len] = code.reshape(16, seq_len)
seq_len = code.shape[0] // num_quantizers
codes[idx, :, :seq_len] = code.reshape(num_quantizers, seq_len)
else:
codes = input_ids.reshape(1, 16, -1)
codes = input_ids.reshape(1, num_quantizers, -1)
else:
if seq_token_counts is None:
logger.debug(
"Code2Wav warmup input length %s is not divisible by 16; padding with zeros.",
"Code2Wav warmup input length %s is not divisible by %s; padding with zeros.",
input_ids.shape[0],
num_quantizers,
)
else:
logger.warning_once(
"Code2Wav input length is not divisible by 16; padding with zeros. "
"This is expected only during cudagraph warmup."
"Code2Wav input length is not divisible by %s; padding with zeros. "
"This is expected only during cudagraph warmup.",
num_quantizers,
)
input_ids_flatten = input_ids.reshape(-1)
input_ids_flatten = torch.cat(
[
input_ids_flatten,
torch.zeros(16 - input_ids.shape[0] % 16, dtype=torch.long, device=input_ids.device),
torch.zeros(
num_quantizers - input_ids.shape[0] % num_quantizers,
dtype=torch.long,
device=input_ids.device,
),
]
)
codes = input_ids_flatten.reshape(1, 16, -1)
codes = input_ids_flatten.reshape(1, num_quantizers, -1)
# Generate audio from codec codes
# Get every request's left_context_size from runtime_additional_information (passed via kwargs)
@@ -541,6 +658,38 @@ class Qwen3OmniMoeForConditionalGeneration(
logger.debug("No additional_information provided to code2wav stage.")
audio_tensors = self.generate_audio(codes, left_context_size, seq_token_counts)
# Keep the skip decision attached to this forward's output, not
# mutable model state: asynchronous materialization can overlap steps.
from vllm_omni.data_entry_keys import FIRST_AUDIO_REQUIRED_KEY
flags = [
bool(info.get("meta", {}).get("first_audio", False)) for info in (runtime_additional_information or [])
]
if flags and any(flags):
if len(flags) != len(audio_tensors):
raise ValueError("First-audio flags must align with Code2Wav requests")
sample_rate = defs.resolve_audio_sample_rate(self.code2wav_config)
frame_counts = (
[count // num_quantizers for count in seq_token_counts] if seq_token_counts else [codes.shape[-1]]
)
# The causal decoder withholds a right-edge tail. The first
# frame has fewer samples than total_upsample; trim its actual
# prefix length, including when chunk 0 contains several frames.
prefix_lengths = [
max(0, audio.numel() - (frames - 1) * int(self.code2wav.total_upsample)) if skip else 0
for audio, frames, skip in zip(audio_tensors, frame_counts, flags, strict=True)
]
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"model_outputs": [
audio.reshape(1, -1)[..., prefix:]
for audio, prefix in zip(audio_tensors, prefix_lengths, strict=True)
],
"sr": [torch.tensor(sample_rate, dtype=torch.int32) for _ in audio_tensors],
FIRST_AUDIO_REQUIRED_KEY: [torch.tensor(skip) for skip in flags],
},
)
return audio_tensors
# Fallback (shouldn't reach here)
@@ -605,7 +754,13 @@ class Qwen3OmniMoeForConditionalGeneration(
if "runtime_additional_information" in kwargs and "model_intermediate_buffer" not in kwargs:
logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer")
# MRv2 passes each request's token span.
spans = kwargs.get("request_token_spans")
if spans is not None and self.eager_frames_active and self.vllm_config.model_config.async_chunk:
return self._make_eager_talker_output(talker_hidden, spans)
code_predictor_codes = [info.get("codes", {}).get("audio") for info in info_dicts]
if spans is not None:
code_predictor_codes = self._align_codes_to_spans(code_predictor_codes, spans, talker_hidden.device)
audio_codes = torch.cat(code_predictor_codes, dim=0)
multimodal_outputs: OmniPayload = {"codes": {"audio": audio_codes}}
span_len = audio_codes.shape[0]
@@ -627,6 +782,44 @@ class Qwen3OmniMoeForConditionalGeneration(
return model_outputs
def _align_codes_to_spans(
self,
codes: list[Any],
spans: list[tuple[int, int]],
device: torch.device,
) -> list[torch.Tensor]:
"""One ``codes.audio`` block per request with exactly its span's rows.
MRv2 also runs requests that never went through preprocess (vLLM's
``_warmup_`` requests), whose buffers carry no codes; they get zero
rows, which the downstream processor skips like prefill rows.
"""
q = int(self.talker.num_code_groups)
aligned: list[torch.Tensor] = []
for block, (start, end) in zip(codes, spans, strict=True):
if isinstance(block, torch.Tensor) and block.ndim == 2 and block.shape[0] == end - start:
aligned.append(block)
else:
aligned.append(torch.zeros((end - start, q), dtype=torch.long, device=device))
return aligned
def _make_eager_talker_output(self, hidden: torch.Tensor, spans: list[tuple[int, int]]) -> OmniOutput:
"""Async-chunk output when the MRv2 runner completes frames after sampling.
``run_eager_mtp`` writes each sampled row's frame and validity into the
last token row of its request span; every other row carries no frame.
"""
num_tokens = spans[-1][1] if spans else 0
q = int(self.talker.num_code_groups)
mm: OmniPayload = {
"codes": {"audio": torch.zeros((num_tokens, q), dtype=torch.long, device=hidden.device)},
"meta": {
"codec_frame_valid": torch.zeros((num_tokens,), dtype=torch.int8, device=hidden.device),
"first_audio": torch.zeros((num_tokens,), dtype=torch.int8, device=hidden.device),
},
}
return OmniOutput(text_hidden_states=hidden, multimodal_outputs=mm)
# ==================== Audio Generation ====================
def generate_audio(
@@ -849,7 +1042,7 @@ class Qwen3OmniMoeForConditionalGeneration(
def _proj_from_thinker(x_opt: torch.Tensor | None) -> torch.Tensor:
if isinstance(x_opt, torch.Tensor) and x_opt.numel() > 0:
xin = _ensure_1x1(x_opt).to(module_device)
xin = to_device_nonblocking(_ensure_1x1(x_opt), module_device)
else:
xin = torch.zeros(
(1, thinker_embed.shape[-1]),
@@ -882,27 +1075,26 @@ class Qwen3OmniMoeForConditionalGeneration(
voice_type = str(voice_type).lower().strip()
start_index = meta.get("num_processed_tokens", 0)
end_index = start_index + input_embeds.shape[0]
# Read thinker outputs for prefill
thinker_sequence_embeds = embed["prefill"].to(
device=self._module_device(self.talker), dtype=torch.bfloat16
# Read thinker outputs for prefill. Every copy here is H2D from the
# connector payload; pageable copies would stall the whole Talker batch.
talker_device = self._module_device(self.talker)
thinker_sequence_embeds = to_device_nonblocking(embed["prefill"], talker_device).to(
torch.bfloat16
) # Tensor [P,H]
thinker_hidden_states = hs["output"].to(
device=self._module_device(self.talker), dtype=torch.bfloat16
) # Tensor [K,H]
thinker_hidden_states = to_device_nonblocking(hs["output"], talker_device).to(torch.bfloat16) # Tensor [K,H]
# Token ids stay on CPU too: prefill parsing reads them on the host.
thinker_sequences_cpu = None if ids.get("all") is None else _cpu_long(ids["all"])
thinker_chatml_ids_cpu = None if ids.get("prompt") is None else _cpu_long(ids["prompt"])
thinker_sequences = (
ids.get("all")
if ids.get("all") is None
else torch.as_tensor(ids["all"], device=self._module_device(self.talker))
None if thinker_sequences_cpu is None else to_device_nonblocking(thinker_sequences_cpu, talker_device)
)
thinker_chatml_ids = (
ids.get("prompt")
if ids.get("prompt") is None
else torch.as_tensor(ids["prompt"], device=self._module_device(self.talker))
None if thinker_chatml_ids_cpu is None else to_device_nonblocking(thinker_chatml_ids_cpu, talker_device)
)
tts_bos_thinker = embed["tts_bos"].to(device=self._module_device(self.talker), dtype=torch.bfloat16)
tts_eos_thinker = embed["tts_eos"].to(device=self._module_device(self.talker), dtype=torch.bfloat16)
tts_pad_thinker = embed["tts_pad"].to(device=self._module_device(self.talker), dtype=torch.bfloat16)
tts_bos_thinker = to_device_nonblocking(embed["tts_bos"], talker_device).to(torch.bfloat16)
tts_eos_thinker = to_device_nonblocking(embed["tts_eos"], talker_device).to(torch.bfloat16)
tts_pad_thinker = to_device_nonblocking(embed["tts_pad"], talker_device).to(torch.bfloat16)
if thinker_sequence_embeds is None or thinker_hidden_states is None:
raise ValueError(
@@ -924,6 +1116,8 @@ class Qwen3OmniMoeForConditionalGeneration(
)
if ids_chatml.ndim == 1:
ids_chatml = ids_chatml.unsqueeze(0)
if thinker_chatml_ids_cpu is not None and thinker_chatml_ids_cpu.ndim == 1:
thinker_chatml_ids_cpu = thinker_chatml_ids_cpu.unsqueeze(0)
else:
# Fallback: create dummy ids if not provided
ids_chatml = torch.zeros(
@@ -932,6 +1126,7 @@ class Qwen3OmniMoeForConditionalGeneration(
device=self._module_device(self.talker),
)
thinker_sequences = ids_chatml
thinker_chatml_ids_cpu = thinker_sequences_cpu = None
speaker_id = self._get_text_spk_token_id(voice_type)
req_input_ids, req_embeds, trailing_text_hidden = self._thinker_to_talker_prefill(
@@ -944,6 +1139,8 @@ class Qwen3OmniMoeForConditionalGeneration(
tts_bos_thinker=tts_bos_thinker,
tts_eos_thinker=tts_eos_thinker,
tts_pad_thinker=tts_pad_thinker,
input_ids_cpu=thinker_chatml_ids_cpu,
thinker_result_ids_cpu=thinker_sequences_cpu,
)
# Queue trailing_text_hidden for decode (drop first for next steps),
@@ -993,12 +1190,11 @@ class Qwen3OmniMoeForConditionalGeneration(
if cached_thinker_decode_embeds is None:
update_dict.setdefault("embed", {})["cached_decode"] = thinker_decode_embeds
else:
cached_thinker_decode_embeds = cached_thinker_decode_embeds.to(
device=self._module_device(self.talker), dtype=torch.bfloat16
)
thinker_decode_embeds = thinker_decode_embeds.to(
device=self._module_device(self.talker), dtype=torch.bfloat16
talker_device = self._module_device(self.talker)
cached_thinker_decode_embeds = to_device_nonblocking(cached_thinker_decode_embeds, talker_device).to(
torch.bfloat16
)
thinker_decode_embeds = to_device_nonblocking(thinker_decode_embeds, talker_device).to(torch.bfloat16)
update_dict.setdefault("embed", {})["cached_decode"] = torch.cat(
[cached_thinker_decode_embeds, thinker_decode_embeds], dim=0
)
@@ -1015,26 +1211,31 @@ class Qwen3OmniMoeForConditionalGeneration(
tts_bos_thinker: torch.Tensor | None = None,
tts_eos_thinker: torch.Tensor | None = None,
tts_pad_thinker: torch.Tensor | None = None,
input_ids_cpu: torch.Tensor | None = None,
thinker_result_ids_cpu: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
"""
Project thinker outputs to talker inputs during prefill stage.
``input_ids_cpu`` / ``thinker_result_ids_cpu`` (host copies of the same
ids) move segment parsing off the GPU: each ``.item()`` / ``nonzero`` /
mask test on device ids is a host sync that stalls the Talker batch.
Returns:
(input_ids, input_embeds) for talker
"""
target_len = thinker_result_ids.shape[-1]
im_start_indexes = torch.cat(
(
torch.nonzero(input_ids[0] == self.config.im_start_token_id).squeeze(-1),
torch.tensor([target_len], device=input_ids.device, dtype=input_ids.dtype),
),
dim=-1,
) # Shape [n_starts + 1]; Take batch 0 since batched inference is not supported here.
# Take batch 0 since batched inference is not supported here.
parse_ids = input_ids_cpu if input_ids_cpu is not None else input_ids
result_ids = thinker_result_ids_cpu if thinker_result_ids_cpu is not None else thinker_result_ids
im_start_indexes = torch.nonzero(parse_ids[0] == self.config.im_start_token_id).flatten().tolist()
im_start_indexes.append(target_len)
multimodal_mask = (
(thinker_result_ids == self.thinker_config.audio_token_id) |
(thinker_result_ids == self.thinker_config.image_token_id) |
(thinker_result_ids == self.thinker_config.video_token_id)
).to(input_ids.device) # [t] # fmt: skip
(result_ids == self.thinker_config.audio_token_id) |
(result_ids == self.thinker_config.image_token_id) |
(result_ids == self.thinker_config.video_token_id)
) # [t] # fmt: skip
role_tokens = parse_ids[0].tolist()
tts_bos_embed, tts_eos_embed, tts_pad_embed = self._get_tts_embed(
thinker_embed, tts_bos_thinker, tts_eos_thinker, tts_pad_thinker
@@ -1045,21 +1246,21 @@ class Qwen3OmniMoeForConditionalGeneration(
trailing_text_hidden_all: torch.Tensor | None = None
# For every chatml parts
for i in range(len(im_start_indexes) - 1):
im_start_index = im_start_indexes[i].item()
segment_end_index = im_start_indexes[i + 1].item()
role_token = input_ids[0][im_start_index + 1]
im_start_index = im_start_indexes[i]
segment_end_index = im_start_indexes[i + 1]
role_token = role_tokens[im_start_index + 1]
# Talker should ignore thinker system prompt
if (role_token == self.config.system_token_id).item():
if role_token == self.config.system_token_id:
continue
# Talker takes word embeddings for tokens and hidden state from `accept_hidden_layer` for multimodal inputs
elif (role_token == self.config.user_token_id).item():
elif role_token == self.config.user_token_id:
talker_user_part = self._get_talker_user_parts(
im_start_index, segment_end_index, multimodal_mask, thinker_hidden, thinker_embed
)
talker_input_embeds.append(talker_user_part)
talker_input_ids.append(thinker_result_ids[im_start_index:segment_end_index])
# Take assistant output (for now)
elif (role_token == self.config.assistant_token_id).item() and i == len(im_start_indexes) - 2:
elif role_token == self.config.assistant_token_id and i == len(im_start_indexes) - 2:
talker_assistant_embeds, talker_assistant_ids, trailing_text_hidden = self._get_talker_assistant_parts(
im_start_index,
segment_end_index,
@@ -1078,7 +1279,7 @@ class Qwen3OmniMoeForConditionalGeneration(
except Exception:
pass
# History assistant output (ignore for now)
elif (role_token == self.config.assistant_token_id).item() and i != len(im_start_indexes) - 2:
elif role_token == self.config.assistant_token_id and i != len(im_start_indexes) - 2:
continue
else:
raise AssertionError("Expect role id after <|im_start|> (assistant, user, system)")
@@ -1103,20 +1304,22 @@ class Qwen3OmniMoeForConditionalGeneration(
cached_thinker_decode_embeds = embed.get("cached_decode", None)
thinker_decode_embed = embed.get("decode", None)
if getattr(self.vllm_config.model_config, "use_v2_model_runner", False) and not meta.get("resumable", False):
return self._next_thinker_decode_text_step(
cached_thinker_decode_embeds, thinker_decode_embed, meta, device, update_dict
)
start_index = meta.get("num_processed_tokens", 0)
if cached_thinker_decode_embeds is not None and start_index < cached_thinker_decode_embeds.shape[0]:
cached_thinker_decode_embeds = cached_thinker_decode_embeds.to(device)
cached_thinker_decode_embeds = to_device_nonblocking(cached_thinker_decode_embeds, device)
thinker_embed = cached_thinker_decode_embeds[start_index]
if thinker_decode_embed is not None:
thinker_decode_embed = thinker_decode_embed.to(device)
thinker_decode_embed = to_device_nonblocking(thinker_decode_embed, device)
cached_thinker_decode_embeds = torch.cat([cached_thinker_decode_embeds, thinker_decode_embed], dim=0)
update_dict.setdefault("embed", {})["cached_decode"] = cached_thinker_decode_embeds
elif thinker_decode_embed is not None:
thinker_embed = thinker_decode_embed
if thinker_embed.device != device:
thinker_embed = thinker_embed.to(device)
thinker_embed = to_device_nonblocking(thinker_decode_embed, device)
else:
# When the tokens output by the thinker are exhausted, an EOS token needs to be appended.
@@ -1129,6 +1332,41 @@ class Qwen3OmniMoeForConditionalGeneration(
update_dict.setdefault("embed", {})["decode"] = None
return self.talker.text_projection(thinker_embed).to(device)
def _next_thinker_decode_text_step(
self,
pending: torch.Tensor | None,
incoming: torch.Tensor | None,
meta: OmniPayloadMeta,
device: torch.device,
update_dict: OmniPayload,
) -> torch.Tensor:
"""Take the oldest unconsumed Thinker decode row as this step's text.
Rows arrive in order but not one per Talker step: the MRv2 receiver
hands over every row that arrived since the previous step (the V1
adapter delivers one chunk per step). New rows are appended to the
pending queue ``embed.cached_decode`` and each step consumes one. Once
the queue is empty the Thinker's text is exhausted: EOS once, then pad.
"""
embed_update = update_dict.setdefault("embed", {})
embed_update["decode"] = None
if isinstance(incoming, torch.Tensor) and incoming.numel() > 0:
incoming = to_device_nonblocking(incoming, device).reshape(-1, incoming.shape[-1])
if isinstance(pending, torch.Tensor) and pending.shape[0] > 0:
pending = to_device_nonblocking(pending, device)
pending = torch.cat([pending, incoming.to(pending.dtype)], dim=0)
else:
pending = incoming
if not isinstance(pending, torch.Tensor) or pending.shape[0] == 0:
embed_update["cached_decode"] = None
if meta.get("eos_emitted", False):
return self.tts_pad_embed.to(device)
update_dict.setdefault("meta", {})["eos_emitted"] = True
return self.tts_eos_embed.to(device)
pending = to_device_nonblocking(pending, device)
embed_update["cached_decode"] = pending[1:] if pending.shape[0] > 1 else None
return self.talker.text_projection(pending[:1]).to(device)
def talker_preprocess_decode(
self, input_ids: torch.Tensor, input_embeds: torch.Tensor, update_dict: OmniPayload, payload: OmniPayload
):
@@ -1203,6 +1441,21 @@ class Qwen3OmniMoeForConditionalGeneration(
)
user_mm_mask = multimodal_mask[im_start_index:segment_end_index]
if user_mm_mask.device.type == "cpu":
# Host mask: no device sync for the test, and a text-only segment
# (the common TTS case) needs no masked gather at all.
if not bool(user_mm_mask.any()):
user_thinker_embed = thinker_embed[im_start_index:segment_end_index]
return self.talker.text_projection(user_thinker_embed).to(thinker_hidden.device, torch.bfloat16)
mm_index = to_device_nonblocking(torch.nonzero(user_mm_mask).flatten(), thinker_hidden.device)
text_index = to_device_nonblocking(torch.nonzero(~user_mm_mask).flatten(), thinker_hidden.device)
segment_hidden = thinker_hidden[im_start_index:segment_end_index]
segment_embed = thinker_embed[im_start_index:segment_end_index]
mm_hidden = self.talker.hidden_projection(segment_hidden.index_select(0, mm_index))
user_talker_part.index_copy_(0, mm_index, mm_hidden.to(user_talker_part.dtype))
text_hidden = self.talker.text_projection(segment_embed.index_select(0, text_index))
user_talker_part.index_copy_(0, text_index, text_hidden.to(user_talker_part.dtype))
return user_talker_part
# Multimodal data exists
if user_mm_mask.any():
user_thinker_hidden_mm = thinker_hidden[im_start_index:segment_end_index][user_mm_mask]
@@ -1236,17 +1489,19 @@ class Qwen3OmniMoeForConditionalGeneration(
),
dim=0,
)
codec_special_tokens = torch.tensor(
[
self.config.talker_config.codec_nothink_id,
self.config.talker_config.codec_think_bos_id,
self.config.talker_config.codec_think_eos_id,
speaker_id,
self.config.talker_config.codec_pad_id,
self.config.talker_config.codec_bos_id,
],
device=tts_pad_embed.device,
dtype=torch.long,
codec_special_tokens = to_device_nonblocking(
torch.tensor(
[
self.config.talker_config.codec_nothink_id,
self.config.talker_config.codec_think_bos_id,
self.config.talker_config.codec_think_eos_id,
speaker_id,
self.config.talker_config.codec_pad_id,
self.config.talker_config.codec_bos_id,
],
dtype=torch.long,
),
tts_pad_embed.device,
)
embed_input_ids = self.talker.embed_input_ids(codec_special_tokens).to(
device=tts_pad_embed.device, dtype=torch.bfloat16
@@ -1381,18 +1636,31 @@ class Qwen3OmniMoeForConditionalGeneration(
# ==================== Weight Loading ====================
def _get_codec_frame_config(self) -> tuple[int, int]:
"""Extract codec_chunk_frames and codec_left_context_frames from stage connector config."""
def _get_codec_connector_extra(self) -> dict[str, Any]:
model_cfg = getattr(self.vllm_config, "model_config", None)
connector_cfg = getattr(model_cfg, "stage_connector_config", None)
if isinstance(connector_cfg, dict):
extra = connector_cfg.get("extra", {})
else:
extra = getattr(connector_cfg, "extra", None) or {}
return extra if isinstance(extra, Mapping) else {}
def _get_codec_frame_config(self) -> tuple[int, int]:
"""Extract codec_chunk_frames and codec_left_context_frames from stage connector config."""
extra = self._get_codec_connector_extra()
chunk_frames = int(extra.get("codec_chunk_frames", 0) or 0)
left_frames = int(extra.get("codec_left_context_frames", 0) or 0)
return chunk_frames, left_frames
def _get_codec_ramp_windows(self) -> list[int]:
"""Decode windows of the connector's ``codec_chunk_ramp`` (empty without a ramp)."""
extra = self._get_codec_connector_extra()
chunk_frames = int(extra.get("codec_chunk_frames", 25) or 25)
ramp = parse_chunk_ramp(dict(extra), steady=chunk_frames)
if not ramp:
return []
return ramp_decode_windows(ramp, int(extra.get("codec_left_context_frames", 25) or 0))
def _maybe_enable_code2wav_cudagraph(self) -> None:
"""Enable the inner Code2Wav CUDA graph unless this stage runs in eager mode."""
if not self.code2wav or not hasattr(self.code2wav, "enable_cudagraph"):
@@ -1404,9 +1672,15 @@ class Qwen3OmniMoeForConditionalGeneration(
return
chunk_frames, left_frames = self._get_codec_frame_config()
streaming = bool(self.vllm_config.model_config.async_chunk)
# Opt-in multi-row graphs for streaming windows (connector extra
# ``codec_graph_batch_sizes``, e.g. [2, 3, 4, 6, 8, 12, 16]).
batch_sizes = self._get_codec_connector_extra().get("codec_graph_batch_sizes") or ()
self.code2wav.enable_cudagraph(
codec_chunk_frames=chunk_frames,
codec_left_context_frames=left_frames,
extra_capture_sizes=self._get_codec_ramp_windows() if streaming else (),
streaming_batch_sizes=[int(b) for b in batch_sizes] if streaming else (),
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
@@ -1439,6 +1713,12 @@ class Qwen3OmniMoeForConditionalGeneration(
loaded_weights.update(talker_loaded)
loaded_weights.update(self._init_special_tokens_embeddings())
# The Talker's first-frame decoder is a Code2Wav copy (same code2wav.* weights).
first_frame_decoder = getattr(self, "first_frame_decoder", None)
if first_frame_decoder is not None and code2wav_weights:
decoder_loaded = first_frame_decoder.load_weights(code2wav_weights)
loaded_weights.update(add_prefix_to_loaded_weights(decoder_loaded, "first_frame_decoder.code2wav"))
# Load code2wav weights
if self.code2wav and code2wav_weights:
code2wav_loaded = self.code2wav.load_weights(code2wav_weights)
@@ -5,7 +5,8 @@
from __future__ import annotations
from collections.abc import Iterable
from collections.abc import Callable, Iterable, Sequence
from contextlib import nullcontext
import numpy as np
import torch
@@ -19,6 +20,7 @@ from transformers.models.qwen3_omni_moe.modeling_qwen3_omni_moe import (
Qwen3OmniMoeCode2WavDecoderBlock,
Qwen3OmniMoeCode2WavTransformerModel,
Qwen3OmniMoeConvNeXtBlock,
Qwen3OmniMoeSnakeBeta,
)
from vllm.config import VllmConfig # type: ignore
from vllm.logger import init_logger # type: ignore
@@ -31,9 +33,93 @@ from vllm_omni.model_executor.models.common.snake_activation import SnakeBeta
from vllm_omni.model_executor.models.qwen3_omni.quantization import (
Qwen3OmniNestedSupportsQuant,
)
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
# Fixed cost of one streaming decode call (graph replay or eager launch), in
# decoded-frame equivalents: a call on a single frame costs about as much GPU
# time as decoding this many extra frames inside a larger call.
_DECODE_CALL_COST_FRAMES = 32
def use_fused_snake(module: nn.Module) -> int:
"""Replace HF ``Qwen3OmniMoeSnakeBeta`` activations under ``module`` with the fused one.
The HF decoder blocks recompute ``exp(alpha)`` / ``exp(beta)`` and launch
about seven broadcast kernels per call; the shared ``SnakeBeta`` computes the
same ``x + 1/b * sin^2(a*x)`` in one kernel from precomputed caches.
Parameter names and shapes match, so this runs before weight loading.
"""
count = 0
for parent in list(module.modules()):
for name, child in list(parent.named_children()):
if isinstance(child, Qwen3OmniMoeSnakeBeta):
setattr(parent, name, SnakeBeta(child.in_features))
count += 1
return count
def plan_decode_groups(
lengths: Sequence[int],
bucket_of: Callable[[int], int | None],
batch_sizes_for: Callable[[int], Sequence[int]],
call_cost_frames: int = _DECODE_CALL_COST_FRAMES,
) -> list[tuple[list[int], int]]:
"""Split a streaming batch into decode calls of rows with similar lengths.
Decoding every row at the batch's longest window wastes most of the work
when short ramp chunks (1-15 frames) share a step with steady 50-frame
windows. Rows are sorted by length and cut into contiguous groups of
graph buckets (``bucket_of``) so that the modelled cost, per call
``call_cost_frames`` plus padded rows (to the next captured row count of
that bucket, ``batch_sizes_for``) times the bucket length, is smallest.
Returns ``(row indices, decode length)`` per call; the decode length is
the group's longest row.
"""
count = len(lengths)
if count == 0:
return []
order = sorted(range(count), key=lambda row: lengths[row])
buckets = [bucket_of(int(lengths[row])) or int(lengths[row]) for row in order]
distinct = sorted(set(buckets))
members = [[row for row, bucket in zip(order, buckets) if bucket == size] for size in distinct]
def sizes_for(size: int) -> list[int]:
return sorted({int(b) for b in batch_sizes_for(size) if int(b) > 0} | {1})
def cost(rows: int, size: int) -> int:
batch_sizes = sizes_for(size)
calls, rest = divmod(rows, batch_sizes[-1])
padded = calls * batch_sizes[-1]
if rest:
calls += 1
padded += next(b for b in batch_sizes if b >= rest)
return calls * call_cost_frames + padded * size
best = [0] + [None] * len(distinct)
cut = [0] * (len(distinct) + 1)
for end in range(1, len(distinct) + 1):
rows = 0
for start in range(end - 1, -1, -1):
rows += len(members[start])
total = best[start] + cost(rows, distinct[end - 1])
if best[end] is None or total < best[end]:
best[end], cut[end] = total, start
groups: list[tuple[list[int], int]] = []
end = len(distinct)
while end > 0:
start = cut[end]
groups.append(([row for bucket_rows in members[start:end] for row in bucket_rows], distinct[end - 1]))
end = start
calls: list[tuple[list[int], int]] = []
for rows, size in reversed(groups):
max_batch = sizes_for(size)[-1]
for offset in range(0, len(rows), max_batch):
part = rows[offset : offset + max_batch]
calls.append((part, max(int(lengths[row]) for row in part)))
return calls
class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
"""
@@ -74,6 +160,14 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
super().__init__()
self.config: Qwen3OmniMoeCode2WavConfig = vllm_config.model_config.hf_config
connector = getattr(vllm_config.model_config, "stage_connector_config", None)
extra = connector.get("extra", {}) if isinstance(connector, dict) else getattr(connector, "extra", {})
# Both the separate stage and the Talker's first-frame copy receive
# this connector config. Keep algorithm selection consistent between
# them, and leave other models' cuDNN settings unchanged.
self._cudnn_benchmark = (extra or {}).get("codec_cudnn_benchmark", False)
if not isinstance(self._cudnn_benchmark, bool):
raise ValueError("codec_cudnn_benchmark must be a boolean")
# Calculate total upsampling factor
self.total_upsample = np.prod(self.config.upsample_rates + self.config.upsampling_ratios)
@@ -122,10 +216,19 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
Qwen3OmniMoeCausalConvNet(output_dim, 1, kernel_size=7),
]
self.decoder = nn.ModuleList(decoder)
# Keep existing V1 and non-CUDA decoder blocks unchanged.
if (
(extra or {}).get("codec_fused_snake", False)
and current_omni_platform.is_cuda()
and torch.device(vllm_config.device_config.device).type == "cuda"
):
use_fused_snake(self.decoder)
# CUDA Graph support — reuses CUDAGraphDecoderWrapper from Qwen3-TTS
self._cudagraph_enabled = False
self._cudagraph_wrapper = None
# Captured row counts per graph size; empty keeps one padded decode per batch.
self._streaming_batch_sizes: dict[int, list[int]] = {}
def precompute_snake_caches(self):
"""Precompute exp(alpha) and 1/(exp(beta)+eps) for all SnakeBeta modules."""
@@ -142,8 +245,19 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
device: torch.device | None = None,
codec_chunk_frames: int = 0,
codec_left_context_frames: int = 0,
extra_capture_sizes: Iterable[int] = (),
streaming_batch_sizes: Iterable[int] = (),
):
"""Enable CUDA graph acceleration (same pattern as Qwen3-TTS Code2Wav)."""
"""Enable CUDA graph acceleration (same pattern as Qwen3-TTS Code2Wav).
``extra_capture_sizes`` adds exact-size graphs (e.g. a chunk ramp's
decode windows) to the default buckets, so those chunks replay without
zero padding. ``streaming_batch_sizes`` (e.g. ``[2, 4, 8, 16]``) also
captures multi-row graphs for every bucket up to one streaming window
(chunk + left context); a streaming batch is then decoded as a few
length groups (``plan_decode_groups``) instead of one eager call
padded to its longest row.
"""
from vllm_omni.model_executor.models.qwen3_tts.cuda_graph_decoder_wrapper import (
CUDAGraphDecoderWrapper,
)
@@ -154,8 +268,30 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
logger.warning("Cannot enable CUDA Graph: not on CUDA device (got %s)", device)
return
extra = {int(size) for size in extra_capture_sizes if int(size) > 0}
batch_sizes = sorted({int(b) for b in streaming_batch_sizes if int(b) > 1})
capture_sizes = None
extra_shapes: list[tuple[int, int]] = []
if extra or batch_sizes:
capture_sizes = sorted(
set(
CUDAGraphDecoderWrapper.compute_capture_sizes(
codec_chunk_frames=codec_chunk_frames,
codec_left_context_frames=codec_left_context_frames,
)
)
| extra
)
window = codec_chunk_frames + codec_left_context_frames
streaming_sizes = [size for size in capture_sizes if window <= 0 or size <= window]
# A multi-row graph never holds more frames than the largest
# single-row one, so the shared graph pool does not grow.
largest = max(capture_sizes)
extra_shapes = [(b, size) for b in batch_sizes for size in streaming_sizes if b * size <= largest]
wrapper = CUDAGraphDecoderWrapper(
decoder=self,
capture_sizes=capture_sizes,
extra_capture_shapes=extra_shapes,
num_quantizers=self.config.num_quantizers,
enabled=True,
)
@@ -172,10 +308,18 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
raise
self._cudagraph_wrapper = wrapper
self._cudagraph_enabled = True
if extra_shapes and torch.cuda.is_available():
# Return the eager warm-up activations: this stage shares the GPU.
torch.accelerator.empty_cache()
self._streaming_batch_sizes = {
size: [1, *sorted(b for b, shape_size in extra_shapes if shape_size == size)]
for size in {shape_size for _b, shape_size in extra_shapes}
}
logger.info(
"CUDA Graph enabled for Code2Wav: num_quantizers=%d, sizes=%s",
"CUDA Graph enabled for Code2Wav: num_quantizers=%d, sizes=%s, multi-row shapes=%s",
self.config.num_quantizers,
self._cudagraph_wrapper.capture_sizes,
extra_shapes,
)
def forward(self, codes: torch.Tensor) -> torch.Tensor:
@@ -200,6 +344,30 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
hidden = self.pre_transformer(inputs_embeds=hidden).last_hidden_state
# Shape: [batch, seq_len, hidden_size]
# Algorithm search happens during eager warmup before graph capture.
# Restore process settings afterward, including on a failed decode.
# Captured replays use the selected kernels without this Python path.
cudnn = torch.backends.cudnn
context = (
cudnn.flags(
enabled=cudnn.enabled,
benchmark=True,
benchmark_limit=10,
deterministic=cudnn.deterministic,
allow_tf32=cudnn.allow_tf32,
)
if (
self._cudnn_benchmark
and hidden.is_cuda
and torch.version.hip is None
and not torch.cuda.is_current_stream_capturing()
)
else nullcontext()
)
with context:
return self._decode_waveform(hidden)
def _decode_waveform(self, hidden: torch.Tensor) -> torch.Tensor:
# Stage 3: Upsampling
hidden = hidden.permute(0, 2, 1) # [batch, hidden_size, seq_len]
for blocks in self.upsample:
@@ -305,28 +473,38 @@ class Qwen3OmniMoeCode2Wav(nn.Module, Qwen3OmniNestedSupportsQuant):
"defaulting to left_context_size=zeros(len(codes)). This is expected during cudagraph warmup."
)
left_context_size = [0] * codes.shape[0]
# Decode chunk
wavs = []
if self._cudagraph_enabled and self._cudagraph_wrapper is not None:
batch_wav = self._cudagraph_wrapper.decode(codes)
else:
batch_wav = self(codes)
if seq_token_counts is not None:
code_seq_lens = [n // self.config.num_quantizers for n in seq_token_counts]
else:
# Fallback: assume all batch elements share the same sequence length.
code_seq_lens = [codes.shape[-1]] * codes.shape[0]
# The decoder trims the same right-edge tail from every batch row.
# Infer it from the decoded window, not a shorter request's length:
# padding that request with codec zeros does not provide valid context.
tail = max(0, int(codes.shape[-1] * self.total_upsample) - batch_wav.shape[-1])
for idx, code_seq_len in enumerate(code_seq_lens):
# Refill the previous chunk's tail using the re-decoded left context,
# and exclude the current row's padded tail just as singleton decode does.
start = max(0, left_context_size[idx] * self.total_upsample - tail)
end = max(0, code_seq_len * self.total_upsample - tail)
wav_chunk = batch_wav[idx, :, start:end]
wavs.append(wav_chunk)
batch, width = int(codes.shape[0]), int(codes.shape[-1])
graphs = self._cudagraph_wrapper if self._cudagraph_enabled else None
batch_sizes = getattr(self, "_streaming_batch_sizes", None)
if graphs is not None and batch_sizes and batch > 1:
groups = plan_decode_groups(
code_seq_lens, graphs._get_padded_size, lambda size: batch_sizes.get(size, (1,))
)
else:
groups = [(list(range(batch)), width)]
wavs: list[torch.Tensor | None] = [None] * batch
for rows, length in groups:
if len(rows) == batch and length == width:
group_codes = codes
else:
# Basic indexing only: a device index tensor would need a host copy.
group_codes = torch.stack([codes[row, :, :length] for row in rows])
batch_wav = graphs.decode(group_codes) if graphs is not None else self(group_codes)
# The decoder trims the same right-edge tail from every batch row.
# Infer it from the decoded window, not a shorter request's length:
# padding that request with codec zeros does not provide valid context.
tail = max(0, int(length * self.total_upsample) - batch_wav.shape[-1])
for position, row in enumerate(rows):
# Refill the previous chunk's tail using the re-decoded left context,
# and exclude the current row's padded tail just as singleton decode does.
start = max(0, left_context_size[row] * self.total_upsample - tail)
end = max(0, code_seq_lens[row] * self.total_upsample - tail)
wavs[row] = batch_wav[position, :, start:end]
return wavs
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
@@ -18,8 +18,7 @@ from vllm_omni.model_executor.models.common.qwen3_code_predictor import (
CodePredictorDecoderLayer,
_rotate_half,
)
from .short_kv_attention import short_kv_attention
from vllm_omni.model_executor.models.common.short_kv_attention import short_kv_attention
def _attention_cached(
@@ -4,9 +4,7 @@
from typing import Any
import torch
from vllm_omni.platforms import current_omni_platform
from vllm_omni.model_executor.models.common.talker_first_audio import supports_talker_first_audio
def talker_first_audio_enabled(vllm_config: Any) -> bool:
@@ -18,16 +16,4 @@ def talker_first_audio_enabled(vllm_config: Any) -> bool:
from .qwen3_tts_code_predictor_vllm import Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM as Predictor
extra = Predictor._stage_connector_extra_config(vllm_config)
model = vllm_config.model_config
parallel = vllm_config.parallel_config
return (
Predictor._parse_bool_config(extra.get("talker_first_audio"))
and current_omni_platform.is_cuda()
and torch.device(vllm_config.device_config.device).type == "cuda"
and bool(getattr(model, "use_v2_model_runner", False))
and bool(getattr(model, "async_chunk", False))
and parallel.tensor_parallel_size == 1
and parallel.pipeline_parallel_size == 1
and parallel.distributed_executor_backend in (None, "uni")
and not vllm_config.cache_config.enable_prefix_caching
)
return Predictor._parse_bool_config(extra.get("talker_first_audio")) and supports_talker_first_audio(vllm_config)
@@ -0,0 +1,312 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# ruff: noqa: N803 - Triton constexpr parameters use kernel-style capitals.
"""Fused residual-codebook predictor for Qwen3-TTS (CUDA, BF16).
Same computation as the frame-local-KV predictor with per-call top-k Gumbel
sampling, restructured to issue few kernels per residual step, since at serving
batch sizes each of the 15 dependent steps is launch- and latency-bound:
- Per layer: RMSNorm, one QKV GEMM, one attention kernel (q/k RMSNorm, RoPE,
this call's K/V write and attention over the frame's keys), O projection
accumulated into the residual stream (``addmm_``), RMSNorm, gate/up GEMM,
SiLU-mul, down projection accumulated into the stream.
- The embedding of each sampled code followed by ``small_to_mtp_projection``
is linear, so it folds at load into one ``[vocab, hidden]`` table per
codebook; the sampling kernel writes the next step's input row directly.
- Sampling finds the top-k threshold by bisection over the BF16 order keys
(exact k-th largest, ties kept) instead of a sort.
"""
from __future__ import annotations
import torch
from vllm.triton_utils import tl, tldevice, triton
from .tokenizer_12hz.streaming_decoder import _silu_mul_kernel
@triton.jit
def _rms_rows_kernel(x_ptr, x_stride, w_ptr, out_ptr, eps, N: tl.constexpr):
row = tl.program_id(0).to(tl.int64)
offs = tl.arange(0, N)
x = tl.load(x_ptr + row * x_stride + offs).to(tl.float32)
var = tl.sum(x * x, axis=0) / N
dt = out_ptr.dtype.element_ty
y = (x * tl.rsqrt(var + eps)).to(dt).to(tl.float32)
w = tl.load(w_ptr + offs).to(tl.float32)
tl.store(out_ptr + row * N + offs, (w * y).to(dt))
@triton.jit
def _bf(x):
return x.to(tl.bfloat16).to(tl.float32)
@triton.jit
def _head_norm_rope(x1, x2, w1, w2, c1, c2, s1, s2, eps, HD: tl.constexpr):
"""HF RMSNorm (``weight * normed.to(bf16)``) then RoPE in BF16 elementwise steps, on the two halves."""
var = (tl.sum(x1 * x1, axis=0) + tl.sum(x2 * x2, axis=0)) / HD
r = tl.rsqrt(var + eps)
n1 = _bf(w1 * _bf(x1 * r))
n2 = _bf(w2 * _bf(x2 * r))
# rotate_half(n) = [-n2, n1]
o1 = _bf(_bf(n1 * c1) + _bf(-n2 * s1))
o2 = _bf(_bf(n2 * c2) + _bf(n1 * s2))
return o1, o2
@triton.jit
def _cp_attention_kernel(
qkv_ptr, # [B * NQ, (H + 2 * KVH) * HD]
kc_ptr, # [Bmax, KVH, MAXP, HD]
vc_ptr,
qw_ptr,
kw_ptr,
cos_ptr, # [MAXP, HD] bf16
sin_ptr,
out_ptr, # [B * NQ, H * HD]
first,
eps,
scale,
NQ: tl.constexpr,
H: tl.constexpr,
KVH: tl.constexpr,
HD: tl.constexpr,
MAXP: tl.constexpr,
BK: tl.constexpr,
):
b = tl.program_id(0)
h = tl.program_id(1)
qi = tl.program_id(2)
kvh = h // (H // KVH)
W: tl.constexpr = (H + 2 * KVH) * HD
HALF: tl.constexpr = HD // 2
d = tl.arange(0, HALF)
p = first + qi
row = (b * NQ + qi).to(tl.int64)
qw1 = tl.load(qw_ptr + d).to(tl.float32)
qw2 = tl.load(qw_ptr + HALF + d).to(tl.float32)
kw1 = tl.load(kw_ptr + d).to(tl.float32)
kw2 = tl.load(kw_ptr + HALF + d).to(tl.float32)
qb = qkv_ptr + row * W + h * HD
q1, q2 = _head_norm_rope(
tl.load(qb + d).to(tl.float32), tl.load(qb + HALF + d).to(tl.float32), qw1, qw2,
tl.load(cos_ptr + p * HD + d).to(tl.float32), tl.load(cos_ptr + p * HD + HALF + d).to(tl.float32),
tl.load(sin_ptr + p * HD + d).to(tl.float32), tl.load(sin_ptr + p * HD + HALF + d).to(tl.float32),
eps, HD,
) # fmt: skip
j = tl.arange(0, BK)
cache = ((b * KVH + kvh) * MAXP).to(tl.int64) * HD
old = (j < first)[:, None]
kp = kc_ptr + cache + j[:, None] * HD + d[None, :]
vp = vc_ptr + cache + j[:, None] * HD + d[None, :]
k1 = tl.load(kp, mask=old, other=0.0).to(tl.float32)
k2 = tl.load(kp + HALF, mask=old, other=0.0).to(tl.float32)
v1 = tl.load(vp, mask=old, other=0.0).to(tl.float32)
v2 = tl.load(vp + HALF, mask=old, other=0.0).to(tl.float32)
for n in tl.static_range(NQ):
# this call's position first + n, computed from its own QKV row
on = first + n
kb = qkv_ptr + (b * NQ + n).to(tl.int64) * W + (H + kvh) * HD
vb = qkv_ptr + (b * NQ + n).to(tl.int64) * W + (H + KVH + kvh) * HD
kn1, kn2 = _head_norm_rope(
tl.load(kb + d).to(tl.float32), tl.load(kb + HALF + d).to(tl.float32), kw1, kw2,
tl.load(cos_ptr + on * HD + d).to(tl.float32), tl.load(cos_ptr + on * HD + HALF + d).to(tl.float32),
tl.load(sin_ptr + on * HD + d).to(tl.float32), tl.load(sin_ptr + on * HD + HALF + d).to(tl.float32),
eps, HD,
) # fmt: skip
vn1 = tl.load(vb + d).to(tl.float32)
vn2 = tl.load(vb + HALF + d).to(tl.float32)
sel = (j == on)[:, None]
k1 = tl.where(sel, kn1[None, :], k1)
k2 = tl.where(sel, kn2[None, :], k2)
v1 = tl.where(sel, vn1[None, :], v1)
v2 = tl.where(sel, vn2[None, :], v2)
wm = (d < HALF) & (qi == n) & (h % (H // KVH) == 0)
dtc = kc_ptr.dtype.element_ty
tl.store(kc_ptr + cache + on * HD + d, kn1.to(dtc), mask=wm)
tl.store(kc_ptr + cache + on * HD + HALF + d, kn2.to(dtc), mask=wm)
tl.store(vc_ptr + cache + on * HD + d, vn1.to(dtc), mask=wm)
tl.store(vc_ptr + cache + on * HD + HALF + d, vn2.to(dtc), mask=wm)
km = j <= p
s = (tl.sum(k1 * q1[None, :], axis=1) + tl.sum(k2 * q2[None, :], axis=1)) * scale
s = tl.where(km, s, -float("inf"))
e = tl.exp(s - tl.max(s, axis=0))
e = tl.where(km, e, 0.0)
w = e / tl.sum(e, axis=0)
ob = out_ptr + row * (H * HD) + h * HD
dto = out_ptr.dtype.element_ty
tl.store(ob + d, tl.sum(v1 * w[:, None], axis=0).to(dto))
tl.store(ob + HALF + d, tl.sum(v2 * w[:, None], axis=0).to(dto))
@triton.jit
def _cp_sample_kernel(
logits_ptr, # [B, V] bf16
u_ptr, # uniforms, row stride u_stride, contiguous over V
u_stride,
codes_ptr, # [B, G] int64
step,
G: tl.constexpr,
table_ptr, # [V, HID] next-step input table, or dummy
next_ptr, # [B, HID]
inv_temperature,
V: tl.constexpr,
TOPK: tl.constexpr,
HID: tl.constexpr,
HAS_NEXT: tl.constexpr,
):
row = tl.program_id(0).to(tl.int64)
idx = tl.arange(0, V)
value = tl.load(logits_ptr + row * V + idx)
scaled = (value.to(tl.float32) * inv_temperature).to(value.dtype)
if TOPK > 0:
# order-preserving 16-bit keys of the BF16 values
bits = scaled.to(tl.int16, bitcast=True).to(tl.int32) & 0xFFFF
# Top-k keeps all numeric ties at the cutoff, including both zeros.
bits = tl.where((bits & 0x7FFF) == 0, 0, bits)
key = tl.where(bits >= 0x8000, 0xFFFF - bits, bits | 0x8000)
the = 0
for bit in tl.static_range(15, -1, -1):
cand = the | (1 << bit)
cnt = tl.sum((key >= cand).to(tl.int32), axis=0)
the = tl.where(cnt >= TOPK, cand, the)
sf = tl.where(key >= the, scaled.to(tl.float32), -float("inf"))
else:
sf = scaled.to(tl.float32)
u = tl.load(u_ptr + row * u_stride + idx)
score = sf - tldevice.log(-tldevice.log(u))
max_score = tl.max(score, 0)
chosen = tl.min(tl.where(score == max_score, idx, V), 0)
first_nan = tl.min(tl.where(score != score, idx, V), 0)
code = tl.where(first_nan < V, first_nan, chosen)
tl.store(codes_ptr + row * G + step, code.to(tl.int64))
if HAS_NEXT:
c = tl.arange(0, HID)
tl.store(next_ptr + row * HID + c, tl.load(table_ptr + code.to(tl.int64) * HID + c))
class FusedCodePredictor:
"""Execution object built from a loaded ``Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM``."""
def __init__(self, predictor, max_batch: int) -> None:
model = predictor.model
weight = next(model.parameters())
dev, dt = weight.device, weight.dtype
if dt != torch.bfloat16:
raise ValueError("fused code predictor requires BF16 weights")
cfg = predictor.config
self.groups = int(cfg.num_code_groups)
self.vocab = int(cfg.vocab_size)
self.hidden = int(cfg.hidden_size)
attn0 = model.layers[0].self_attn
self.heads, self.kv_heads, self.head_dim = attn0.num_heads, attn0.num_kv_heads, attn0.head_dim
self.scale = attn0.scaling
self.eps = float(model.norm.variance_epsilon)
self.max_pos = attn0.max_seq
self.max_batch = max_batch
f32 = lambda t: t.detach().to(device=dev, dtype=torch.float32) # noqa: E731
c = lambda t: t.to(dt).contiguous() # noqa: E731
proj = predictor.small_to_mtp_projection
if isinstance(proj, torch.nn.Identity):
pw, pb = None, None
else:
pw, pb = f32(proj.weight), f32(proj.bias)
self.proj_wt = c(pw.t()) if pw is not None else None
self.proj_b = c(pb) if pb is not None else None
tables = []
for emb in model.codec_embedding:
e = f32(emb.weight)
tables.append(c(e @ pw.t() + pb) if pw is not None else c(e))
self.tables = tables
self.layers = []
for layer in model.layers:
a = layer.self_attn
self.layers.append(
dict(
ln1=c(f32(layer.input_layernorm.weight)),
ln2=c(f32(layer.post_attention_layernorm.weight)),
eps1=float(layer.input_layernorm.variance_epsilon),
eps2=float(layer.post_attention_layernorm.variance_epsilon),
qkv=c(f32(a.qkv_proj.weight).t()),
qn=c(f32(a.q_norm.weight)),
kn=c(f32(a.k_norm.weight)),
head_eps=float(a.q_norm.variance_epsilon),
o=c(f32(a.o_proj.weight).t()),
gu=c(f32(layer.mlp.gate_up_proj.weight).t()),
down=c(f32(layer.mlp.down_proj.weight).t()),
)
)
if a.qkv_proj.bias is not None:
raise ValueError("fused code predictor assumes bias-free attention")
self.norm_w = c(f32(model.norm.weight))
self.lm_heads = [c(f32(h.weight).t()) for h in predictor.lm_head]
rot = model.rotary_emb
self.cos = rot.cos_cached.to(device=dev, dtype=dt).contiguous()
self.sin = rot.sin_cached.to(device=dev, dtype=dt).contiguous()
kv = (len(self.layers), 2, max_batch, self.kv_heads, self.max_pos, self.head_dim)
self.cache = torch.zeros(kv, device=dev, dtype=dt)
self._dummy = torch.zeros(1, device=dev, dtype=dt)
def _layer(self, x: torch.Tensor, li: int, B: int, nq: int, first: int) -> None:
L = self.layers[li]
rows = x.shape[0]
h = torch.empty_like(x)
_rms_rows_kernel[(rows,)](x, x.stride(0), L["ln1"], h, L["eps1"], N=self.hidden)
qkv = torch.mm(h, L["qkv"])
att = torch.empty(rows, self.heads * self.head_dim, device=x.device, dtype=x.dtype)
_cp_attention_kernel[(B, self.heads, nq)](
qkv, self.cache[li, 0], self.cache[li, 1], L["qn"], L["kn"], self.cos, self.sin, att,
first, L["head_eps"], self.scale,
NQ=nq, H=self.heads, KVH=self.kv_heads, HD=self.head_dim, MAXP=self.max_pos,
BK=triton.next_power_of_2(self.max_pos), num_warps=1,
) # fmt: skip
x.addmm_(att, L["o"])
_rms_rows_kernel[(rows,)](x, x.stride(0), L["ln2"], h, L["eps2"], N=self.hidden)
gu = torch.mm(h, L["gu"])
inter = gu.shape[1] // 2
a = torch.empty(rows, inter, device=x.device, dtype=x.dtype)
_silu_mul_kernel[(rows, triton.cdiv(inter, 1024))](gu, a, N=inter, BN=1024)
x.addmm_(a, L["down"])
@torch.inference_mode()
def __call__(
self,
layer0_code: torch.Tensor,
layer0_embed: torch.Tensor,
last_talker_hidden: torch.Tensor,
inv_temperature: float,
top_k: int,
sample_uniforms: torch.Tensor,
) -> torch.Tensor:
B = int(layer0_code.shape[0])
if B > self.max_batch:
raise ValueError("fused code predictor batch exceeds its capacity")
dev, dt = layer0_embed.device, self.norm_w.dtype
inp = torch.stack([last_talker_hidden.reshape(B, -1), layer0_embed.reshape(B, -1)], 1).to(dt)
inp = inp.reshape(2 * B, -1)
x = torch.addmm(self.proj_b, inp, self.proj_wt) if self.proj_wt is not None else inp.clone()
codes = torch.empty(B, self.groups, device=dev, dtype=torch.int64)
codes[:, 0] = layer0_code.reshape(B)
u = sample_uniforms
nq, first = 2, 0
for step in range(1, self.groups):
for li in range(len(self.layers)):
self._layer(x, li, B, nq, first)
last = x[nq - 1 :: nq] if nq > 1 else x
hf = torch.empty(B, self.hidden, device=dev, dtype=dt)
_rms_rows_kernel[(B,)](last, last.stride(0), self.norm_w, hf, self.eps, N=self.hidden)
logits = torch.mm(hf, self.lm_heads[step - 1])
has_next = step < self.groups - 1
nxt = torch.empty(B, self.hidden, device=dev, dtype=dt) if has_next else self._dummy
us = u[:, step - 1]
_cp_sample_kernel[(B,)](
logits, us, us.stride(0), codes, step, self.groups,
self.tables[step - 1] if has_next else self._dummy, nxt, inv_temperature,
V=self.vocab, TOPK=top_k, HID=self.hidden, HAS_NEXT=has_next, num_warps=4,
) # fmt: skip
x = nxt
first = step + 1
nq = 1
return codes
@@ -72,3 +72,33 @@ QWEN3_TTS_PIPELINE = PipelineConfig(
),
),
)
# Single-stage variant: the Talker decodes each frame with the stateful
# streaming codec decoder and emits PCM as the final output (enable with the
# ``talker_stream_decode`` model option). No Code2Wav stage, no connector.
QWEN3_TTS_FUSED_PIPELINE = PipelineConfig(
model_type="qwen3_tts_fused",
default_deploy_config_name="qwen3_tts_fused_single_gpu.yaml",
single_stage_async_chunk=True,
model_arch="Qwen3TTSTalkerForConditionalGeneration",
stages=(
StagePipelineConfig(
stage_id=0,
model_stage="qwen3_tts",
execution_type=StageExecutionType.LLM_AR,
input_sources=(),
owns_tokenizer=True,
final_output=True,
final_output_type="audio",
# The API output processor tags ``model_outputs`` by this modality.
engine_output_type="audio",
supports_running_prefix_cache_reset=False,
supports_native_mrv2_data_plane=True,
sampling_constraints={
"detokenize": False,
"stop_token_ids": [2150],
},
),
),
)
@@ -42,6 +42,7 @@ from vllm.logger import init_logger
from vllm.multimodal.audio import AudioResampler
from vllm_omni.utils.audio import mel_filter_bank
from vllm_omni.utils.device_copy import index_to_device, to_device_nonblocking
if TYPE_CHECKING:
from .configuration_qwen3_tts import Qwen3TTSConfig, Qwen3TTSTalkerConfig
@@ -160,7 +161,7 @@ def coerce_token_ids(value: object, *, device: torch.device) -> torch.Tensor | N
ids = ids.unsqueeze(0)
if ids.ndim != 2 or ids.numel() == 0:
return None
return ids.to(device=device, dtype=torch.long).contiguous()
return to_device_nonblocking(ids.to(dtype=torch.long).contiguous(), device)
# ---------------------------------------------------------------------------
@@ -366,6 +367,9 @@ class Qwen3TTSPromptEmbedsBuilder:
# Reusing them avoids launching the embedding/projection pair once per
# request during prompt construction.
self._projected_token_cache: dict[tuple[str, tuple[int, ...]], torch.Tensor] = {}
# CustomVoice prompt pieces that do not depend on the text, per
# (device, speaker, language): bounded by the model's voices.
self._custom_voice_prompt_cache: dict[tuple, tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = {}
self._ref_audio_artifact_cache_max_entries = int(ref_audio_artifact_cache_max_entries)
self._ref_audio_artifact_cache: OrderedDict[str, dict[str, torch.Tensor | bool]] = OrderedDict()
@@ -1024,7 +1028,9 @@ class Qwen3TTSPromptEmbedsBuilder:
dev = self._device()
input_ids = coerce_token_ids(info_dict.pop(PRECOMPUTED_TEXT_IDS_KEY, None), device=dev)
if input_ids is None:
input_ids = tok(build_assistant_text(text), return_tensors="pt", padding=False)["input_ids"].to(device=dev)
input_ids = to_device_nonblocking(
tok(build_assistant_text(text), return_tensors="pt", padding=False)["input_ids"], dev
)
# Optional instruct prefix.
instruct = (info_dict.get("instruct") or [""])[0]
@@ -1411,35 +1417,44 @@ class Qwen3TTSPromptEmbedsBuilder:
raise ValueError(f"Unsupported speaker: {speaker}")
spk_id = spk_id_map[speaker]
# Keep it at least 1D; embedding on a 0-d tensor can return 1D.
spk_tensor = torch.tensor([spk_id], device=input_ids.device, dtype=torch.long)
spk_tensor = index_to_device([spk_id], input_ids.device)
spk_embed = codec_embed(spk_tensor)
if spk_embed.ndim in (1, 2):
spk_embed = spk_embed.view(1, 1, -1)
speaker_embed = spk_embed
codec_input = torch.cat([codec_input_0, speaker_embed, codec_input_1], dim=1)
role_embed = text_projection(text_embedding(input_ids[:, :3]))
codec_prefix = torch.cat((tts_pad_embed.expand(-1, codec_input.shape[1] - 2, -1), tts_bos_embed), dim=1)
codec_prefix = codec_prefix + codec_input[:, :-1]
talker_prompt = torch.cat((role_embed, codec_prefix), dim=1)
# The role tokens, speaker/language codec prefix, codec pad row and
# closing row do not depend on the text: cache them per speaker and
# language so a request only embeds and projects its own text.
# A text starting with whitespace could merge with the template's
# newline token, so it keeps the per-request role embedding.
cache_key = None
if non_streaming_mode and instruct_embed is None and not text[:1].isspace():
cache_key = (
str(input_ids.device),
int(spk_id),
codec_prefill_list[0][2] if len(codec_prefill_list[0]) == 4 else None,
)
cv_cache = self._custom_voice_prompt_cache
cached = cv_cache.get(cache_key) if cache_key is not None else None
if cached is None:
role_embed = text_projection(text_embedding(input_ids[:, :3]))
codec_prefix = torch.cat((tts_pad_embed.expand(-1, codec_input.shape[1] - 2, -1), tts_bos_embed), dim=1)
codec_prefix = codec_prefix + codec_input[:, :-1]
prefix = torch.cat((role_embed, codec_prefix), dim=1)
pad_row = codec_embed(self._long_tensor([talker_config.codec_pad_id], input_ids.device)).view(1, 1, -1)
tail = tts_pad_embed + codec_embed(self._long_tensor([talker_config.codec_bos_id], input_ids.device))
if cache_key is not None:
cv_cache[cache_key] = (prefix, pad_row, tail)
else:
prefix, pad_row, tail = cached
talker_prompt = prefix
if non_streaming_mode:
text_all = text_projection(text_embedding(input_ids[:, 3:-5]))
text_all = torch.cat([text_all, tts_eos_embed], dim=1)
pad_ids = torch.full(
(1, int(text_all.shape[1])),
int(talker_config.codec_pad_id),
device=input_ids.device,
dtype=torch.long,
)
talker_prompt = torch.cat(
[
talker_prompt,
text_all + codec_embed(pad_ids),
tts_pad_embed + codec_embed(self._long_tensor([talker_config.codec_bos_id], input_ids.device)),
],
dim=1,
)
talker_prompt = torch.cat([talker_prompt, text_all + pad_row, tail], dim=1)
trailing_text_hidden = tts_pad_embed
else:
first_text = text_projection(text_embedding(input_ids[:, 3:4])) + codec_input[:, -1:]
@@ -55,6 +55,9 @@ class Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM(CodePredictorWrapp
talker_hidden_size=int(talker_config.hidden_size),
prefix=prefix,
)
# TTS owns its frame-local cache and overrides _predict_step_logits.
# Do not also activate the shared wrapper's Omni incremental loop.
self._kv_cache_enabled = False
# Store talker_config for backward compat (accessed by some callers)
self.talker_config = talker_config
self._vllm_config = vllm_config
@@ -62,6 +65,8 @@ class Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM(CodePredictorWrapp
self._kv_requested = self._parse_bool_config(extra.get("code_predictor_kv_cache"))
self._fused_sampling = self._parse_bool_config(extra.get("code_predictor_fused_sampling"))
self._frame_cache: FrameLocalKVCache | None = None
self._fused_requested = self._parse_bool_config(extra.get("code_predictor_fused"))
self._fused = None
def _setup_compile(self) -> None:
if self._compiled_model_fwd is not None:
@@ -97,11 +102,73 @@ class Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM(CodePredictorWrapp
cache(self._proj_buf, batch, step)
self._synchronize_warmup(weight.device)
self._frame_cache = cache
if self._fused_requested:
from .fused_code_predictor import FusedCodePredictor
self._fused = FusedCodePredictor(self, max_batch)
# Compile every kernel variant outside any graph capture.
hidden = int(self.talker_config.hidden_size)
for batch in range(1, min(2, max_batch) + 1):
zeros = torch.zeros(batch, 1, hidden, device=weight.device, dtype=weight.dtype)
uniforms = torch.full(
(batch, self._num_groups - 1, int(self.config.vocab_size)), 0.5, device=weight.device
)
codes = torch.zeros(batch, 1, device=weight.device, dtype=torch.long)
self._fused(codes, zeros, zeros, 1.0, 50, uniforms)
self._synchronize_warmup(weight.device)
logger.info("Qwen3-TTS fused code predictor enabled (capacity %d)", max_batch)
# Preserve the shared wrapper's initialization sentinel. Its full
# re-prefill callable is unused while _frame_cache is active.
self._compiled_model_fwd = self.model.forward
logger.info("Qwen3-TTS frame-local KV warmed for batches %s", self._bucket_sizes)
@torch.inference_mode()
def forward(
self,
layer0_code: torch.Tensor,
layer0_embed: torch.Tensor,
last_talker_hidden: torch.Tensor,
do_sample: bool = True,
temperature: float = 0.9,
top_k: int = 50,
top_p: float = 1.0,
generator: torch.Generator | None = None,
generators: Sequence[torch.Generator | None] | None = None,
sample_uniforms: torch.Tensor | None = None,
):
self._validate_sampling_inputs(int(layer0_code.shape[0]), generators, sample_uniforms)
if self._fused_requested:
self._setup_compile()
if (
self._fused is not None
and do_sample
and temperature > 0
and top_p == 1.0
and sample_uniforms is not None
and sample_uniforms.dtype == torch.float32
and sample_uniforms.is_contiguous()
):
return self._fused(
layer0_code,
layer0_embed,
last_talker_hidden,
1.0 / temperature,
int(top_k),
sample_uniforms,
)
return super().forward(
layer0_code,
layer0_embed,
last_talker_hidden,
do_sample=do_sample,
temperature=temperature,
top_k=top_k,
top_p=top_p,
generator=generator,
generators=generators,
sample_uniforms=sample_uniforms,
)
def _predict_step_logits(
self,
proj_buf: torch.Tensor,
@@ -27,6 +27,7 @@ from vllm.sequence import IntermediateTensors
from vllm_omni.data_entry_keys import OmniPayload
from vllm_omni.model_executor.models.output_templates import OmniOutput, OwnedBatchTensor
from vllm_omni.utils.device_copy import index_to_device, to_device_nonblocking
from vllm_omni.utils.speaker_cache import (
get_speaker_cache,
iter_custom_voice_profiles,
@@ -39,6 +40,7 @@ from .configuration_qwen3_tts import Qwen3TTSConfig, Qwen3TTSSpeakerEncoderConfi
from .first_audio import talker_first_audio_enabled
from .prompt_embeds_builder import PRECOMPUTED_TEXT_IDS_KEY, Qwen3TTSPromptEmbedsBuilder, resolve_x_vector_only
from .qwen3_tts_code_predictor_vllm import Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM
from .stream_decode import stream_ref_context_frames, talker_stream_decode_enabled
from .tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Config
from .tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Encoder
@@ -411,7 +413,21 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
# A frame's residual codebooks depend only on the Talker hidden and
# CB0 of the step that sampled it, so MRV2 may complete the frame at
# the end of that step instead of in the next step's preprocess.
self.mtp_eager_frames = talker_first_audio_enabled(vllm_config)
self.stream_decode = talker_stream_decode_enabled(vllm_config)
predictor = Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM
extra = predictor._stage_connector_extra_config(vllm_config)
self.stream_first_audio = self.stream_decode and predictor._parse_bool_config(
extra.get("talker_stream_first_audio")
)
# Model-local voice-clone priming boundary; each deployment selects
# its reference context independently of the two-stage codec profile.
self.stream_ref_context_frames = stream_ref_context_frames(vllm_config) if self.stream_decode else 0
self.mtp_eager_frames = talker_first_audio_enabled(vllm_config) or self.stream_decode
self.stream_decoder = None
self.stream_graphs = None
self.stream_prime_graphs = None
self.stream_sample_rate = 0
self.stream_chunk_frames = 25
# The runners bypass only the outer whole-MTP graph when explicit
# generators are present, so seeded requests can still share one raw
# batched MTP call with independent per-row streams.
@@ -718,6 +734,11 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
# -------------------- Omni multimodal output plumbing --------------------
@property
def mm_outputs_fresh_per_step(self) -> bool:
"""Stream-decode outputs come from :meth:`_make_eager_omni_output`, allocated per step."""
return self.stream_decoder is not None and self.eager_frames_active
def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput:
if isinstance(model_outputs, OmniOutput):
return model_outputs
@@ -895,6 +916,13 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
"first_audio": torch.zeros((num_tokens,), dtype=torch.int8, device=hidden.device),
},
}
if self.stream_decoder is not None:
# Filled per sampled row by the runner's eager MTP; token-major so
# each request's slice is its frames of this step.
spf = int(self.stream_decoder.spf)
mm["model_outputs"] = torch.zeros((num_tokens, spf), dtype=torch.float32, device=hidden.device)
sr = torch.tensor(self.stream_sample_rate, dtype=torch.int32)
mm["sr"] = [sr] * len(info_dicts)
ref_rows = [
index
for index, info in enumerate(info_dicts)
@@ -1442,8 +1470,72 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
self.first_frame_decoder = Qwen3TTSFirstFrameDecoder(self.model_path)
decoder_loaded = self.first_frame_decoder.load(self.vllm_config)
loaded = set(loaded) | {f"first_frame_decoder.{name}" for name in decoder_loaded}
elif self.stream_decode:
# The in-stage decoder owns every frame. Optional first-audio
# delivery uses its PCM; later chunks use regular step outputs.
from .first_frame_decoder import Qwen3TTSFirstFrameDecoder
from .tokenizer_12hz.streaming_decoder import StreamingCodecDecoder
holder = Qwen3TTSFirstFrameDecoder(self.model_path)
decoder_loaded = holder.load(self.vllm_config)
loaded = set(loaded) | {f"stream_decoder.{name}" for name in decoder_loaded}
slots = int(self.vllm_config.scheduler_config.max_num_seqs)
self.stream_decoder = StreamingCodecDecoder(holder.decoder, num_slots=slots, dtype=torch.bfloat16)
self.stream_sample_rate = int(holder.sample_rate)
logger.info("Qwen3-TTS Talker stream decode enabled (%d slots)", slots)
return loaded
def capture_stream_decode_graphs(self, batch_sizes: list[int]) -> None:
if self.stream_decoder is None or self.stream_graphs is not None:
return
from .tokenizer_12hz.streaming_decoder import StreamingDecodeGraphs
with torch.inference_mode():
self.stream_graphs = StreamingDecodeGraphs(self.stream_decoder, batch_sizes)
frames = min(self.stream_ref_context_frames, self.stream_chunk_frames, self.stream_decoder.max_frames)
if self.config.tts_model_type == "base" and frames > 1:
# Bound the multi-frame graph workspace by the configured
# token budget; larger priming groups keep the eager path.
budget = max(1, self.vllm_config.scheduler_config.max_num_batched_tokens // frames)
prime_sizes = sorted({1, *(size for size in batch_sizes if size <= budget)})
self.stream_prime_graphs = StreamingDecodeGraphs(self.stream_decoder, prime_sizes, frames=frames)
logger.info("Captured Talker stream decode graphs for batch sizes %s", self.stream_graphs.sizes)
if self.stream_prime_graphs is not None:
logger.info(
"Captured Talker reference priming graphs for %d frames and batch sizes %s",
self.stream_prime_graphs.frames,
self.stream_prime_graphs.sizes,
)
def get_stream_ref_context(self, info: OmniPayload) -> torch.Tensor | None:
"""Model-owned reference-code layout and context boundary for a PCM slot."""
codes = info.get("codes")
ref = codes.get("ref") if isinstance(codes, dict) else None
if not isinstance(ref, torch.Tensor) or not ref.numel():
return None
return ref.reshape(-1, int(self.talker_config.num_code_groups))[-self.stream_ref_context_frames :]
def prime_stream_decoder(self, primes: list[tuple[int, torch.Tensor]]) -> None:
"""Prime equal-length reference groups without padding or aliasing live slots."""
if not primes:
return
stream = self.stream_decoder
if stream is None:
raise RuntimeError("Reference priming requires the single-stage PCM decoder")
groups: dict[int, list[tuple[int, torch.Tensor]]] = {}
for req_idx, ref in primes:
groups.setdefault(int(ref.shape[0]), []).append((req_idx, ref))
for items in groups.values():
codes = to_device_nonblocking(torch.stack([ref for _idx, ref in items]), stream.device).to(torch.int32)
n = len(items)
slots = index_to_device([idx for idx, _ref in items], stream.device, dtype=torch.int32)
for t0 in range(0, int(codes.shape[1]), self.stream_chunk_frames):
pos = index_to_device([t0] * n, stream.device, dtype=torch.int32)
chunk = codes[:, t0 : t0 + self.stream_chunk_frames].contiguous()
graphs = self.stream_prime_graphs
decode = graphs if graphs is not None and chunk.shape[1] == graphs.frames else stream
decode(chunk, slots, pos)
def _build_stacked_codec_embed(self) -> None:
embeds = self.code_predictor.get_input_embeddings()
if not embeds:
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Eligibility and model options for the single-stage CUDA PCM pipeline."""
import torch
from vllm.config import VllmConfig
from vllm_omni.platforms import current_omni_platform
def talker_stream_decode_enabled(vllm_config: VllmConfig) -> bool:
"""The Talker decodes every frame itself and emits PCM as its final output."""
from .qwen3_tts_code_predictor_vllm import Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM as Predictor
extra = Predictor._stage_connector_extra_config(vllm_config)
model = vllm_config.model_config
parallel = vllm_config.parallel_config
requested = Predictor._parse_bool_config(extra.get("talker_stream_decode"))
if not requested:
if getattr(model, "engine_output_type", None) == "audio":
raise ValueError("The single-stage Qwen3-TTS pipeline requires talker_stream_decode=True")
if Predictor._parse_bool_config(extra.get("talker_stream_first_audio")):
raise ValueError("talker_stream_first_audio requires the single-stage talker_stream_decode path")
return False
if getattr(model, "engine_output_type", None) != "audio" or not getattr(model, "final_output", False):
raise ValueError(
"talker_stream_decode requires a final audio-output Talker; "
"select qwen3_tts_fused_single_gpu.yaml instead of enabling it on the two-stage pipeline"
)
if getattr(model, "supports_running_prefix_cache_reset", True):
raise ValueError(
"The single-stage PCM pipeline must disable running prefix-cache reset; "
"select qwen3_tts_fused_single_gpu.yaml"
)
if not (
current_omni_platform.is_cuda()
and torch.device(vllm_config.device_config.device).type == "cuda"
and bool(getattr(model, "use_v2_model_runner", False))
and bool(getattr(model, "async_chunk", False))
and parallel.tensor_parallel_size == 1
and parallel.pipeline_parallel_size == 1
and parallel.distributed_executor_backend in (None, "uni")
and not vllm_config.cache_config.enable_prefix_caching
):
raise ValueError(
"talker_stream_decode requires CUDA, MRv2, async_chunk=True, TP=PP=1, "
"an in-process worker and enable_prefix_caching=False; "
"use qwen3_tts_fused_single_gpu.yaml"
)
if Predictor._parse_bool_config(extra.get("talker_first_audio")):
raise ValueError("talker_stream_decode and talker_first_audio are mutually exclusive")
return True
def stream_ref_context_frames(vllm_config: VllmConfig) -> int:
"""Reference-code context frames for voice-clone priming in the Talker."""
from .qwen3_tts_code_predictor_vllm import Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM as Predictor
extra = Predictor._stage_connector_extra_config(vllm_config)
return int(extra.get("ref_code_context_frames") or extra.get("codec_left_context_frames", 25))
@@ -0,0 +1,799 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# ruff: noqa: N803 - Triton constexpr parameters use kernel-style capitals.
"""Stateful, fused streaming decoder for the Qwen3-TTS 12 Hz codec.
Every layer of the codec decoder is causal, so what a new frame needs from the
past is small and fixed: each causal conv's last ``(k - 1) * dilation`` input
rows, each overlapping transposed conv's last GEMM row, and each attention
layer's last ``sliding_window`` keys and values. A request owns a slot of
per-layer state holding exactly that, and a call decodes ``T`` new frames for
each of ``B`` requests, reading and advancing the state. The output equals the
whole-utterance decode (``Qwen3TTSTokenizerV2Decoder._forward_exact``) frame by
frame, up to floating point rounding: nothing is recomputed and nothing is
truncated.
Layout and fusion:
- Activations are time-major ``[rows, C]``; every dense conv is a tap-major
im2col (one Triton kernel that also applies the pending bias and SnakeBeta,
and reads/writes the history) followed by one cuBLAS GEMM.
- Linear chains fold at load: the RVQ output projections, ``pre_conv`` and the
transformer's ``input_proj`` become one ``k = 3`` conv over the concatenated
codebook embeddings; LayerScale folds into ``o_proj``/``down_proj``; ConvNeXt
``gamma`` folds into ``pwconv2``.
- A bias that feeds a residual stream is carried to that stream's consumer
(the next im2col or transposed-conv GEMM) instead of being added in place.
- Residual branches accumulate into their stream with in-place ``addmm_``.
History buffers are double-buffered per slot: a call reads half ``parity[slot]``
and writes the other half, then flips the parity, so no kernel overwrites rows
another program of the same launch still reads. A row whose first frame index
``pos`` is 0 reads zeros, which are the convs' causal padding, and attention
only looks back ``min(pos + 1, window)`` positions, so a new request needs no
state reset.
"""
from __future__ import annotations
from dataclasses import dataclass
import torch
from torch import nn
from vllm.triton_utils import tl, triton
from vllm_omni.platforms import current_omni_platform
_KERNEL = 7
# --------------------------------------------------------------------------- kernels
@triton.jit
def _rvq_gather_kernel(codes_ptr, books_ptr, out_ptr, NQ: tl.constexpr, CB: tl.constexpr, HALF: tl.constexpr):
row = tl.program_id(0)
offs = tl.arange(0, HALF)
c0 = tl.load(codes_ptr + row * NQ).to(tl.int64)
c0 = tl.where((c0 >= 0) & (c0 < CB), c0, 0) # EOS/special rows decode as code 0; callers drop them
first = tl.load(books_ptr + c0 * HALF + offs).to(tl.float32)
acc = tl.zeros([HALF], dtype=tl.float32)
for q in range(1, NQ):
c = tl.load(codes_ptr + row * NQ + q).to(tl.int64)
c = tl.where((c >= 0) & (c < CB), c, 0)
acc += tl.load(books_ptr + (q * CB + c) * HALF + offs).to(tl.float32)
dt = out_ptr.dtype.element_ty
tl.store(out_ptr + row * 2 * HALF + offs, first.to(dt))
tl.store(out_ptr + row * 2 * HALF + HALF + offs, acc.to(dt))
@triton.jit
def _fast_sin(y):
# Two-step reduction to [-pi, pi], then the hardware approximation
# (absolute error ~4e-7 there): accurate sin's instruction count, not
# memory, bounds every kernel that applies SnakeBeta.
k = tl.extra.cuda.libdevice.rint(y * 0.15915494309189535)
r = tl.fma(-k, -1.7484555314695172e-07, tl.fma(-k, 6.2831854820251465, y))
return tl.inline_asm_elementwise("sin.approx.f32 $0, $1;", "=r,r", [r], dtype=tl.float32, is_pure=True, pack=1)
@triton.jit
def _snake(v, a, ib):
s = _fast_sin(v * a)
return v + ib * s * s
@triton.jit
def _im2col_kernel(
x_ptr, # [B * T, C] layer input rows
bias_ptr, # [C] pending bias (added to x rows only) or dummy
alpha_ptr, # [C] exp(alpha) or dummy
ibeta_ptr, # [C] 1 / (exp(beta) + eps) or dummy
hist_ptr, # [2, S, H, C] transformed history rows
slots_ptr,
pos_ptr,
par_ptr,
col_ptr, # [B * T, K * C]
T,
S,
C: tl.constexpr,
K: tl.constexpr,
D: tl.constexpr,
H: tl.constexpr,
HAS_BIAS: tl.constexpr,
HAS_SNAKE: tl.constexpr,
BU: tl.constexpr,
BC: tl.constexpr,
EXT: tl.constexpr = False,
):
"""Source row ``u`` of ``[history (H) | x (T)]``: scatter to every tap that reads it, save the last H."""
b = tl.program_id(0)
u = tl.program_id(1) * BU + tl.arange(0, BU)
c = tl.program_id(2) * BC + tl.arange(0, BC)
cm = c < C
n_src = H + T
um = u < n_src
slot = tl.load(slots_ptr + b).to(tl.int64)
live = tl.load(pos_ptr + b) != 0
par = tl.load(par_ptr + slot).to(tl.int64)
# history rows
from_hist = u < H
hrow = tl.where(from_hist, u, 0)
hv = tl.load(
hist_ptr + ((par * S + slot) * H + hrow)[:, None] * C + c[None, :],
mask=(from_hist & um & live)[:, None] & cm[None, :],
other=0.0,
).to(tl.float32)
xrow = tl.where(from_hist, 0, u - H)
xv = tl.load(
x_ptr + (b * T + xrow).to(tl.int64)[:, None] * C + c[None, :],
mask=((~from_hist) & um)[:, None] & cm[None, :],
other=0.0,
).to(tl.float32)
if HAS_BIAS:
xv += tl.load(bias_ptr + c, mask=cm, other=0.0).to(tl.float32)[None, :]
if HAS_SNAKE:
a = tl.load(alpha_ptr + c, mask=cm, other=0.0).to(tl.float32)[None, :]
ib = tl.load(ibeta_ptr + c, mask=cm, other=0.0).to(tl.float32)[None, :]
xv = _snake(xv, a, ib)
dt = col_ptr.dtype.element_ty
v = tl.where(from_hist[:, None], hv, xv.to(dt).to(tl.float32)).to(dt)
if EXT:
tl.store(col_ptr + (b * n_src + u).to(tl.int64)[:, None] * C + c[None, :], v, mask=um[:, None] & cm[None, :])
else:
for j in tl.static_range(K):
t = u - j * D
tm = (t >= 0) & (t < T) & um
tl.store(
col_ptr + (b * T + tl.where(tm, t, 0)).to(tl.int64)[:, None] * (K * C) + j * C + c[None, :],
v,
mask=tm[:, None] & cm[None, :],
)
if H > 0:
hm = (u >= T) & um
tl.store(
hist_ptr + (((1 - par) * S + slot) * H + tl.where(hm, u - T, 0))[:, None] * C + c[None, :],
v,
mask=hm[:, None] & cm[None, :],
)
@triton.jit
def _rms_norm_kernel(x_ptr, w_ptr, out_ptr, eps, N: tl.constexpr):
row = tl.program_id(0).to(tl.int64)
offs = tl.arange(0, N)
x = tl.load(x_ptr + row * N + offs).to(tl.float32)
var = tl.sum(x * x, axis=0) / N
dt = out_ptr.dtype.element_ty
y = (x * tl.rsqrt(var + eps)).to(dt).to(tl.float32)
w = tl.load(w_ptr + offs).to(tl.float32)
tl.store(out_ptr + row * N + offs, (w * y).to(dt))
@triton.jit
def _rope_kv_write_kernel(
qkv_ptr, # [B * T, 3 * NH * HD]
q_ptr, # [B * T, NH * HD]
k_ring_ptr, # [S, RING, NH * HD]
v_ring_ptr,
slots_ptr,
pos_ptr,
T,
theta,
NH: tl.constexpr,
HD: tl.constexpr,
RING: tl.constexpr,
):
r = tl.program_id(0)
h = tl.program_id(1)
b = r // T
p = tl.load(pos_ptr + b) + r % T
slot = tl.load(slots_ptr + b).to(tl.int64)
half: tl.constexpr = HD // 2
i = tl.arange(0, half)
inv_freq = tl.exp(-(2.0 * i.to(tl.float32) / HD) * tl.log(theta))
ang = p.to(tl.float32) * inv_freq
dt = q_ptr.dtype.element_ty
cos = tl.cos(ang).to(dt).to(tl.float32)
sin = tl.sin(ang).to(dt).to(tl.float32)
W: tl.constexpr = NH * HD
base = qkv_ptr + r.to(tl.int64) * 3 * W + h * HD
q1 = tl.load(base + i).to(tl.float32)
q2 = tl.load(base + half + i).to(tl.float32)
k1 = tl.load(base + W + i).to(tl.float32)
k2 = tl.load(base + W + half + i).to(tl.float32)
vv = tl.arange(0, HD)
v = tl.load(base + 2 * W + vv)
qo = q_ptr + r.to(tl.int64) * W + h * HD
tl.store(qo + i, (q1 * cos - q2 * sin).to(dt))
tl.store(qo + half + i, (q2 * cos + q1 * sin).to(dt))
ring = (slot * RING + p % RING) * W + h * HD
tl.store(k_ring_ptr + ring + i, (k1 * cos - k2 * sin).to(dt))
tl.store(k_ring_ptr + ring + half + i, (k2 * cos + k1 * sin).to(dt))
tl.store(v_ring_ptr + ring + vv, v)
@triton.jit
def _attend_kernel(
q_ptr, # [B * T, NH * HD]
k_ring_ptr,
v_ring_ptr,
out_ptr, # [B * T, NH * HD]
slots_ptr,
pos_ptr,
T,
scale,
NH: tl.constexpr,
HD: tl.constexpr,
RING: tl.constexpr,
WINDOW: tl.constexpr,
BK: tl.constexpr,
):
r = tl.program_id(0)
h = tl.program_id(1)
b = r // T
p = tl.load(pos_ptr + b) + r % T
slot = tl.load(slots_ptr + b).to(tl.int64)
W: tl.constexpr = NH * HD
d = tl.arange(0, HD)
q = tl.load(q_ptr + r.to(tl.int64) * W + h * HD + d).to(tl.float32)
j = tl.arange(0, BK)
n = tl.minimum(p + 1, WINDOW)
km = j < n
kp = p - j # key positions p, p-1, ...
idx = (slot * RING + tl.where(km, kp, 0) % RING) * W + h * HD
k = tl.load(k_ring_ptr + idx[:, None] + d[None, :], mask=km[:, None], other=0.0).to(tl.float32)
s = tl.sum(k * q[None, :], axis=1) * scale
s = tl.where(km, s, -float("inf"))
m = tl.max(s, axis=0)
e = tl.exp(s - m)
e = tl.where(km, e, 0.0)
w = e / tl.sum(e, axis=0)
dt = out_ptr.dtype.element_ty
w = w.to(dt).to(tl.float32)
v = tl.load(v_ring_ptr + idx[:, None] + d[None, :], mask=km[:, None], other=0.0).to(tl.float32)
o = tl.sum(v * w[:, None], axis=0)
tl.store(out_ptr + r.to(tl.int64) * W + h * HD + d, o.to(dt))
@triton.jit
def _silu_mul_kernel(gu_ptr, out_ptr, N: tl.constexpr, BN: tl.constexpr):
row = tl.program_id(0).to(tl.int64)
offs = tl.program_id(1) * BN + tl.arange(0, BN)
m = offs < N
g = tl.load(gu_ptr + row * 2 * N + offs, mask=m).to(tl.float32)
u = tl.load(gu_ptr + row * 2 * N + N + offs, mask=m).to(tl.float32)
dt = out_ptr.dtype.element_ty
sg = (g / (1.0 + tl.exp(-g))).to(dt).to(tl.float32)
tl.store(out_ptr + row * N + offs, (sg * u).to(dt), mask=m)
@triton.jit
def _dwconv_ln_kernel(
z_ptr, # [B * T, C] ConvNeXt input rows (true values)
w_ptr, # [K, C]
b_ptr, # [C]
lnw_ptr,
lnb_ptr,
hist_ptr, # [2, S, K - 1, C]
slots_ptr,
pos_ptr,
par_ptr,
out_ptr, # [B * T, C] LayerNorm output
T,
S,
eps,
C: tl.constexpr,
K: tl.constexpr,
):
b = tl.program_id(0)
t = tl.program_id(1) # 0 .. max(T, K - 1)
H: tl.constexpr = K - 1
c = tl.arange(0, C)
slot = tl.load(slots_ptr + b).to(tl.int64)
live = tl.load(pos_ptr + b) != 0
par = tl.load(par_ptr + slot).to(tl.int64)
dt = out_ptr.dtype.element_ty
hbase = hist_ptr + (par * S + slot) * H * C
acc = tl.load(b_ptr + c).to(tl.float32)
for j in tl.static_range(K):
u = t + j # index into [hist (H) | z (T)]
fh = u < H
hv = tl.load(hbase + tl.where(fh, u, 0) * C + c, mask=(c < C) & fh & live, other=0.0).to(tl.float32)
zrow = (b * T + tl.where(fh, 0, u - H)).to(tl.int64)
zv = tl.load(z_ptr + zrow * C + c, mask=(c < C) & (~fh) & (t < T), other=0.0).to(tl.float32)
acc += tl.where(fh, hv, zv) * tl.load(w_ptr + j * C + c).to(tl.float32)
h = acc.to(dt).to(tl.float32)
mean = tl.sum(h, axis=0) / C
hc = h - mean
var = tl.sum(hc * hc, axis=0) / C
y = hc * tl.rsqrt(var + eps) * tl.load(lnw_ptr + c).to(tl.float32) + tl.load(lnb_ptr + c).to(tl.float32)
tl.store(out_ptr + (b * T + t).to(tl.int64) * C + c, y.to(dt), mask=(c < C) & (t < T))
# new history row t: source row T + t of [hist | z]
u = T + t
fh = u < H
hv = tl.load(hbase + tl.where(fh, u, 0) * C + c, mask=(c < C) & fh & live & (t < H), other=0.0)
zrow = (b * T + tl.where(fh, 0, u - H)).to(tl.int64)
zv = tl.load(z_ptr + zrow * C + c, mask=(c < C) & (~fh) & (t < H), other=0.0)
tl.store(
hist_ptr + (((1 - par) * S + slot) * H + tl.where(t < H, t, 0)) * C + c,
tl.where(fh, hv, zv).to(dt),
mask=(c < C) & (t < H),
)
@triton.jit
def _bias_gelu_kernel(x_ptr, b_ptr, N: tl.constexpr, BN: tl.constexpr):
row = tl.program_id(0).to(tl.int64)
offs = tl.program_id(1) * BN + tl.arange(0, BN)
m = offs < N
x = tl.load(x_ptr + row * N + offs, mask=m).to(tl.float32) + tl.load(b_ptr + offs, mask=m).to(tl.float32)
dt = x_ptr.dtype.element_ty
x = x.to(dt).to(tl.float32)
y = 0.5 * x * (1.0 + tl.erf(x * 0.7071067811865476))
tl.store(x_ptr + row * N + offs, y.to(dt), mask=m)
@triton.jit
def _col2im_kernel(
z_ptr, # [B * T, 2 * R, C] transposed-conv GEMM rows
bias_ptr, # [C]
prev_ptr, # [2, S, R, C] previous call's last GEMM row, second half
slots_ptr,
pos_ptr,
par_ptr,
out_ptr, # [B * T * R, C]
T,
S,
R: tl.constexpr,
C: tl.constexpr,
BC: tl.constexpr,
):
row = tl.program_id(0) # b * T + t
j = tl.program_id(1) # 0 .. R - 1
c = tl.program_id(2) * BC + tl.arange(0, BC)
cm = c < C
b = row // T
t = row % T
slot = tl.load(slots_ptr + b).to(tl.int64)
live = tl.load(pos_ptr + b) != 0
par = tl.load(par_ptr + slot).to(tl.int64)
zr = z_ptr + row.to(tl.int64) * 2 * R * C
cur = tl.load(zr + j * C + c, mask=cm).to(tl.float32)
prev_in = tl.load(zr - 2 * R * C + (R + j) * C + c, mask=cm & (t > 0), other=0.0).to(tl.float32)
prev_st = tl.load(prev_ptr + ((par * S + slot) * R + j) * C + c, mask=cm & (t == 0) & live, other=0.0).to(
tl.float32
)
y = cur + tl.where(t > 0, prev_in, prev_st) + tl.load(bias_ptr + c, mask=cm).to(tl.float32)
dt = out_ptr.dtype.element_ty
tl.store(out_ptr + (row.to(tl.int64) * R + j) * C + c, y.to(dt), mask=cm)
last = tl.load(zr + (R + j) * C + c, mask=cm & (t == T - 1))
tl.store(prev_ptr + (((1 - par) * S + slot) * R + j) * C + c, last, mask=cm & (t == T - 1))
@triton.jit
def _conv_out_kernel(
ext_ptr, # [B, H + T, C] transformed rows: history then this call's
w_ptr, # [K, C]
wb, # conv bias (float)
out_ptr, # [B, T] float32 waveform
T,
C: tl.constexpr,
K: tl.constexpr,
BT: tl.constexpr,
CP: tl.constexpr,
):
b = tl.program_id(0)
t = tl.program_id(1) * BT + tl.arange(0, BT)
c = tl.arange(0, CP)
cm = c < C
tm = t < T
base = ext_ptr + b.to(tl.int64) * (T + K - 1) * C
acc = tl.zeros([BT], dtype=tl.float32)
for j in tl.static_range(K):
v = tl.load(base + (t + j)[:, None] * C + c[None, :], mask=tm[:, None] & cm[None, :], other=0.0).to(tl.float32)
acc += tl.sum(v * tl.load(w_ptr + j * C + c, mask=cm, other=0.0).to(tl.float32)[None, :], axis=1)
dt = ext_ptr.dtype.element_ty
y = acc.to(dt).to(tl.float32) + wb
y = tl.minimum(tl.maximum(y.to(dt).to(tl.float32), -1.0), 1.0)
tl.store(out_ptr + b.to(tl.int64) * T + t, y, mask=tm)
@triton.jit
def _flip_kernel(par_ptr, slots_ptr):
slot = tl.load(slots_ptr + tl.program_id(0)).to(tl.int64)
tl.store(par_ptr + slot, 1 - tl.load(par_ptr + slot))
# --------------------------------------------------------------------------- module
@dataclass
class _ConvSpec:
weight: torch.Tensor # [K * C_in, C_out] tap-major
c_in: int
k: int
d: int
hist: torch.Tensor | None = None # [2, S, H, C_in]
def _conv_taps(w: torch.Tensor) -> torch.Tensor:
"""``[C_out, C_in, K]`` -> ``[K * C_in, C_out]``."""
c_out, c_in, k = w.shape
return w.permute(2, 1, 0).reshape(k * c_in, c_out)
def _snake_params(module) -> tuple[torch.Tensor, torch.Tensor]:
# SnakeBeta: x + 1 / (exp(beta) + 1e-9) * sin(exp(alpha) * x) ** 2
alpha = module.alpha.detach().float()
beta = module.beta.detach().float()
return torch.exp(alpha).contiguous(), (1.0 / (torch.exp(beta) + 1e-9)).contiguous()
class StreamingCodecDecoder(nn.Module):
"""Frame-exact stateful decoder built from a loaded ``Qwen3TTSTokenizerV2Decoder``."""
RING = 128 # KV ring positions; a call may carry up to RING - window + 1 frames
def __init__(self, decoder: nn.Module, num_slots: int, dtype: torch.dtype | None = None) -> None:
super().__init__()
cfg = decoder.config
dev = next(decoder.parameters()).device
dtype = dtype or next(decoder.parameters()).dtype
self.dtype, self.device = dtype, dev
self.num_slots = num_slots + 1 # last slot: scratch for padding rows
self.scratch_slot = num_slots
S = self.num_slots
self.nq = int(cfg.num_quantizers)
self.cb = int(cfg.codebook_size)
self.half = int(cfg.codebook_dim) // 2
self.hidden = int(cfg.hidden_size)
self.heads = int(cfg.num_attention_heads)
self.head_dim = int(cfg.head_dim)
self.window = int(cfg.sliding_window)
self.theta = float(cfg.rope_theta)
self.eps = float(cfg.rms_norm_eps)
self.max_frames = self.RING - self.window + 1
self.spf = int(decoder.total_upsample)
if self.heads != int(cfg.num_key_value_heads):
raise ValueError("streaming codec decoder assumes no grouped KV")
f32 = lambda t: t.detach().to(device=dev, dtype=torch.float32) # noqa: E731
cast = lambda t: t.to(dtype).contiguous() # noqa: E731
def hist(h: int, c: int) -> torch.Tensor:
return torch.zeros(2, S, h, c, device=dev, dtype=dtype)
# RVQ books, pre-divided by usage: [NQ * CB, HALF]
q = decoder.quantizer
books = []
for vq in [q.rvq_first.vq.layers[0]] + list(q.rvq_rest.vq.layers):
cbk = vq._codebook
books.append(f32(cbk.embedding_sum) / f32(cbk.cluster_usage).clamp(min=cbk.epsilon)[:, None])
self.books = cast(torch.cat(books))
w_rvq = torch.cat(
[f32(q.rvq_first.output_proj.weight)[:, :, 0], f32(q.rvq_rest.output_proj.weight)[:, :, 0]], 1
)
# pre_conv (k=3) and input_proj folded onto the embedding concat
pre_w, pre_b = f32(decoder.pre_conv.conv.weight), f32(decoder.pre_conv.conv.bias)
pt = decoder.pre_transformer
in_w, in_b = f32(pt.input_proj.weight), f32(pt.input_proj.bias)
taps = torch.stack([in_w @ pre_w[:, :, j] @ w_rvq for j in range(pre_w.shape[2])], dim=2) # [hid, 2H, 3]
self.pre = _ConvSpec(cast(_conv_taps(taps)), 2 * self.half, 3, 1, hist(2, 2 * self.half))
self.pre_b = cast(in_w @ pre_b + in_b)
# transformer
self.layers = []
qd = self.heads * self.head_dim
for layer in pt.layers:
a = layer.self_attn
qkv = torch.cat([f32(a.q_proj.weight), f32(a.k_proj.weight), f32(a.v_proj.weight)], 0)
o = f32(a.o_proj.weight) * f32(layer.self_attn_layer_scale.scale)[:, None]
gu = torch.cat([f32(layer.mlp.gate_proj.weight), f32(layer.mlp.up_proj.weight)], 0)
down = f32(layer.mlp.down_proj.weight) * f32(layer.mlp_layer_scale.scale)[:, None]
self.layers.append(
dict(
ln1=cast(f32(layer.input_layernorm.weight)),
ln2=cast(f32(layer.post_attention_layernorm.weight)),
qkv=cast(qkv.t()),
o=cast(o.t()),
gu=cast(gu.t()),
down=cast(down.t()),
k_ring=torch.zeros(S, self.RING, qd, device=dev, dtype=dtype),
v_ring=torch.zeros(S, self.RING, qd, device=dev, dtype=dtype),
)
)
self.norm_w = cast(f32(pt.norm.weight))
self.out_w = cast(f32(pt.output_proj.weight).t())
self.out_b = cast(f32(pt.output_proj.bias))
# upsample: transposed conv (kernel = stride, no overlap) + ConvNeXt
self.ups = []
latent = int(cfg.latent_dim)
pending = torch.zeros(latent, device=dev, dtype=torch.float32)
for trans, block in decoder.upsample:
tw = f32(trans.conv.weight) # [C_in, C_out, r]
r = tw.shape[2]
w = tw.permute(0, 2, 1).reshape(latent, r * latent) # [C_in, r * C_out]
bias = (pending @ w).view(r, latent) + f32(trans.conv.bias)[None, :]
dw = block.dwconv.conv
gamma = f32(block.gamma)
self.ups.append(
dict(
r=r,
w=cast(w),
b=cast(bias.reshape(-1)),
dw_w=cast(f32(dw.weight)[:, 0, :].t()),
dw_b=cast(f32(dw.bias)),
ln_w=cast(f32(block.norm.weight)),
ln_b=cast(f32(block.norm.bias)),
ln_eps=float(block.norm.eps),
pw1=cast(f32(block.pwconv1.weight).t()),
pw1_b=cast(f32(block.pwconv1.bias)),
pw2=cast((f32(block.pwconv2.weight) * gamma[:, None]).t()),
hist=hist(_KERNEL - 1, latent),
)
)
pending = f32(block.pwconv2.bias) * gamma
# conv_in (pending bias of the last ConvNeXt added on load)
dec = decoder.decoder
cin = dec[0].conv
self.conv_in = _ConvSpec(cast(_conv_taps(f32(cin.weight))), latent, _KERNEL, 1, hist(_KERNEL - 1, latent))
self.conv_in_pb = cast(pending)
pending = f32(cin.bias)
# decoder blocks
self.blocks = []
for i, blk in enumerate(dec[1:-2]):
snake, trans, *units = blk.block
c_in = trans.conv.in_channels
c_out = trans.conv.out_channels
rate = trans.conv.stride[0]
tw = f32(trans.conv.weight) # [C_in, C_out, 2 * rate]
entry = dict(
pb=cast(pending),
snake=_snake_params(snake),
rate=rate,
c_in=c_in,
c_out=c_out,
w=cast(tw.permute(0, 2, 1).reshape(c_in, 2 * rate * c_out)),
b=cast(f32(trans.conv.bias)),
prev=torch.zeros(2, S, rate, c_out, device=dev, dtype=dtype),
units=[],
)
pending = torch.zeros(c_out, device=dev, dtype=torch.float32)
for u in units:
c1, c2 = u.conv1.conv, u.conv2.conv
entry["units"].append(
dict(
pb=cast(pending),
act1=_snake_params(u.act1),
conv1=_ConvSpec(
cast(_conv_taps(f32(c1.weight))),
c_out,
_KERNEL,
c1.dilation[0],
hist((_KERNEL - 1) * c1.dilation[0], c_out),
),
b1=cast(f32(c1.bias)),
conv1_w4=(
cast(f32(c1.weight)).unsqueeze(2).contiguous(memory_format=torch.channels_last)
if c1.dilation[0] == 1
else None
),
act2=_snake_params(u.act2),
w2=cast(f32(c2.weight)[:, :, 0].t()),
)
)
pending = pending + f32(c2.bias)
self.blocks.append(entry)
self.out_pb = cast(pending)
self.out_snake = _snake_params(dec[-2])
co = dec[-1].conv
self.out_c = co.in_channels
self.conv_out_w = cast(f32(co.weight)[0].t()) # [K, C]
self.conv_out_b = float(f32(co.bias)[0])
self.conv_out = _ConvSpec(self.conv_out_w, self.out_c, _KERNEL, 1, hist(_KERNEL - 1, self.out_c))
self.parity = torch.zeros(S, device=dev, dtype=torch.int32)
self._dummy = torch.zeros(1, device=dev, dtype=dtype)
def _slot_state(self, slot: int) -> list[torch.Tensor]:
state = [self.parity[slot : slot + 1]]
for layer in self.layers:
state.extend((layer["k_ring"][slot], layer["v_ring"][slot]))
for spec in (self.pre, self.conv_in, self.conv_out):
assert spec.hist is not None
state.append(spec.hist[:, slot])
for up in self.ups:
state.append(up["hist"][:, slot])
for block in self.blocks:
state.append(block["prev"][:, slot])
for unit in block["units"]:
state.append(unit["conv1"].hist[:, slot])
return state
def save_slot(self, slot: int) -> list[torch.Tensor]:
"""Offload a preempted request before its runner slot is recycled."""
return [tensor.to(device="cpu", copy=True) for tensor in self._slot_state(slot)]
def restore_slot(self, slot: int, state: list[torch.Tensor]) -> None:
for target, saved in zip(self._slot_state(slot), state, strict=True):
target.copy_(saved)
# ------------------------------------------------------------------ helpers
def _im2col(self, x, spec: _ConvSpec, T, slots, pos, bias=None, snake=None, ext=False):
k = spec.k
d = spec.d
c = x.shape[1]
h = (k - 1) * d
B = slots.shape[0]
if ext:
col = torch.empty(B * (h + T), c, device=x.device, dtype=self.dtype)
else:
col = torch.empty(B * T, k * c, device=x.device, dtype=self.dtype)
bu = 32
bc = min(128, triton.next_power_of_2(c))
grid = (B, triton.cdiv(h + T, bu), triton.cdiv(c, bc))
a, ib = snake if snake is not None else (self._dummy, self._dummy)
_im2col_kernel[grid](
x, bias if bias is not None else self._dummy, a, ib,
spec.hist if h > 0 else self._dummy, slots, pos, self.parity, col,
T, self.num_slots, C=c, K=k, D=d, H=h,
HAS_BIAS=bias is not None, HAS_SNAKE=snake is not None, BU=bu, BC=bc, EXT=ext,
) # fmt: skip
return col
def _conv_cudnn(self, x, u, T, slots, pos):
"""Undilated unit conv: one source-row pass, then cuDNN on its channels-last view.
The im2col path writes a ``K``-times patch matrix; cuDNN reads the
``[history | new]`` rows directly (same result up to summation order).
"""
spec = u["conv1"]
ext = self._im2col(x, spec, T, slots, pos, bias=u["pb"], snake=u["act1"], ext=True)
b = slots.shape[0]
c = x.shape[1]
x4 = ext.view(b, -1, c).permute(0, 2, 1).unsqueeze(2) # [B, C, 1, H + T], channels-last memory
y = torch.nn.functional.conv2d(x4, u["conv1_w4"]) # [B, N, 1, T], channels-last memory
return y.permute(0, 2, 3, 1).reshape(b * T, -1)
def _snake(self, x, snake, bias):
n, c = x.shape
out = torch.empty_like(x)
bu, bc = 32, min(128, triton.next_power_of_2(c))
one = torch.zeros(1, device=x.device, dtype=torch.int32)
_im2col_kernel[(1, triton.cdiv(n, bu), triton.cdiv(c, bc))](
x, bias, snake[0], snake[1], self._dummy, one, one, self.parity, out,
n, self.num_slots, C=c, K=1, D=1, H=0, HAS_BIAS=True, HAS_SNAKE=True, BU=bu, BC=bc,
) # fmt: skip
return out
# ------------------------------------------------------------------ forward
@torch.no_grad()
def forward(self, codes: torch.Tensor, slots: torch.Tensor, pos: torch.Tensor) -> torch.Tensor:
"""``codes`` [B, T, NQ] int, ``slots``/``pos`` [B] int32 -> waveform [B, T * spf] float32.
``pos[b]`` is the index of the row's first frame in its stream; 0 starts
a new stream in ``slots[b]``. Frames beyond ``max_frames`` per call must
be split by the caller.
"""
B, T, _ = codes.shape
if T > self.max_frames:
raise ValueError(f"at most {self.max_frames} frames per call, got {T}")
dev, dt = codes.device, self.dtype
codes = codes.reshape(B * T, self.nq).to(torch.int32).contiguous()
emb = torch.empty(B * T, 2 * self.half, device=dev, dtype=dt)
_rvq_gather_kernel[(B * T,)](codes, self.books, emb, NQ=self.nq, CB=self.cb, HALF=self.half)
x = torch.addmm(self.pre_b, self._im2col(emb, self.pre, T, slots, pos), self.pre.weight)
hid, qd = self.hidden, self.heads * self.head_dim
rows = B * T
h = torch.empty(rows, hid, device=dev, dtype=dt)
q = torch.empty(rows, qd, device=dev, dtype=dt)
att = torch.empty(rows, qd, device=dev, dtype=dt)
a = torch.empty(rows, self.layers[0]["down"].shape[0], device=dev, dtype=dt)
inter = a.shape[1]
scale = self.head_dim**-0.5
for L in self.layers:
_rms_norm_kernel[(rows,)](x, L["ln1"], h, self.eps, N=hid)
qkv = torch.mm(h, L["qkv"])
_rope_kv_write_kernel[(rows, self.heads)](
qkv, q, L["k_ring"], L["v_ring"], slots, pos, T, self.theta,
NH=self.heads, HD=self.head_dim, RING=self.RING,
) # fmt: skip
_attend_kernel[(rows, self.heads)](
q, L["k_ring"], L["v_ring"], att, slots, pos, T, scale,
NH=self.heads, HD=self.head_dim, RING=self.RING, WINDOW=self.window,
BK=triton.next_power_of_2(self.window),
) # fmt: skip
x.addmm_(att, L["o"])
_rms_norm_kernel[(rows,)](x, L["ln2"], h, self.eps, N=hid)
gu = torch.mm(h, L["gu"])
_silu_mul_kernel[(rows, triton.cdiv(inter, 1024))](gu, a, N=inter, BN=1024)
x.addmm_(a, L["down"])
_rms_norm_kernel[(rows,)](x, self.norm_w, h, self.eps, N=hid)
y = torch.addmm(self.out_b, h, self.out_w) # [rows, latent]
t_rows = T
for U in self.ups:
r = U["r"]
z = torch.addmm(U["b"], y, U["w"]).view(rows * r, -1)
rows, t_rows = rows * r, t_rows * r
c = z.shape[1]
n = torch.empty_like(z)
_dwconv_ln_kernel[(B, max(t_rows, _KERNEL - 1))](
z, U["dw_w"], U["dw_b"], U["ln_w"], U["ln_b"], U["hist"], slots, pos, self.parity, n,
t_rows, self.num_slots, U["ln_eps"], C=c, K=_KERNEL,
) # fmt: skip
p1 = torch.mm(n, U["pw1"])
_bias_gelu_kernel[(rows, triton.cdiv(p1.shape[1], 1024))](p1, U["pw1_b"], N=p1.shape[1], BN=1024)
z.addmm_(p1, U["pw2"])
y = z
x = torch.mm(self._im2col(y, self.conv_in, t_rows, slots, pos, bias=self.conv_in_pb), self.conv_in.weight)
for blk in self.blocks:
s = self._snake(x, blk["snake"], blk["pb"])
zt = torch.mm(s, blk["w"]) # [rows, 2 * rate * c_out]
r, c_out = blk["rate"], blk["c_out"]
x = torch.empty(rows * r, c_out, device=dev, dtype=dt)
bc = min(128, triton.next_power_of_2(c_out))
_col2im_kernel[(rows, r, triton.cdiv(c_out, bc))](
zt, blk["b"], blk["prev"], slots, pos, self.parity, x, t_rows, self.num_slots,
R=r, C=c_out, BC=bc,
) # fmt: skip
rows, t_rows = rows * r, t_rows * r
for u in blk["units"]:
if u.get("conv1_w4") is not None:
c1 = self._conv_cudnn(x, u, t_rows, slots, pos)
else:
col = self._im2col(x, u["conv1"], t_rows, slots, pos, bias=u["pb"], snake=u["act1"])
c1 = torch.mm(col, u["conv1"].weight)
s2 = self._snake(c1, u["act2"], u["b1"])
x.addmm_(s2, u["w2"])
ext = self._im2col(x, self.conv_out, t_rows, slots, pos, bias=self.out_pb, snake=self.out_snake, ext=True)
wav = torch.empty(B, t_rows, device=dev, dtype=torch.float32)
_conv_out_kernel[(B, triton.cdiv(t_rows, 256))](
ext, self.conv_out_w, self.conv_out_b, wav, t_rows,
C=self.out_c, K=_KERNEL, BT=256, CP=triton.next_power_of_2(self.out_c),
) # fmt: skip
_flip_kernel[(B,)](self.parity, slots)
return wav
class StreamingDecodeGraphs:
"""CUDA graphs of ``StreamingCodecDecoder`` for a fixed frame count per batch bucket."""
def __init__(self, sd: StreamingCodecDecoder, batch_sizes: list[int], frames: int = 1) -> None:
self.sd, self.frames = sd, frames
self.graphs: dict[int, tuple] = {}
dev = sd.device
pool = torch.cuda.graph_pool_handle()
for bsz in sorted(set(batch_sizes)):
codes = torch.zeros(bsz, frames, sd.nq, dtype=torch.int32, device=dev)
slots = torch.full((bsz,), sd.scratch_slot, dtype=torch.int32, device=dev)
pos = torch.zeros(bsz, dtype=torch.int32, device=dev)
sd(codes, slots, pos)
current_omni_platform.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, pool=pool):
out = sd(codes, slots, pos)
self.graphs[bsz] = (graph, codes, slots, pos, out)
self.sizes = sorted(self.graphs)
def __call__(self, codes: torch.Tensor, slots: torch.Tensor, pos: torch.Tensor) -> torch.Tensor:
"""``codes`` [n, frames, NQ], ``slots``/``pos`` [n] on device -> waveform view [n, frames * spf]."""
n = int(codes.shape[0])
size = next((b for b in self.sizes if b >= n), None)
if size is None:
return self.sd(codes, slots.to(torch.int32), pos.to(torch.int32))
graph, s_codes, s_slots, s_pos, out = self.graphs[size]
s_codes[:n].copy_(codes)
s_slots[:n].copy_(slots)
s_pos[:n].copy_(pos)
if n < size:
s_slots[n:].fill_(self.sd.scratch_slot)
s_pos[n:].zero_()
graph.replay()
return out[:n]
@@ -483,6 +483,12 @@ _OMNI_MODELS = {
"acoustic",
"MiniMaxMusic3AcousticForConditionalGeneration",
),
## YuE2-3B (text-to-music; single-stage native-AR with terminal NAR/VAE)
"Yue2ForCausalLM": (
"yue2",
"yue2",
"Yue2ForCausalLM",
),
}
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""YuE2-3B text-to-music model package."""
from .pipeline import YUE2_PIPELINE
__all__ = ["YUE2_PIPELINE"]
@@ -0,0 +1,53 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""YuE2-3B pipeline topology.
Single-stage native-AR text-to-music (gepard pattern). The whole
Mixture-of-Transformers checkpoint — AR path, NAR path, projection heads —
lives in one ``model.safetensors``, so both the autoregressive sampling and
the terminal flow-matching + VAE pass run inside one stage and no weights are
duplicated across stages. The VAE decoder is a separate repository
(``m-a-p/YuE2-Vae``), loaded in ``load_weights`` from ``$YUE2_VAE`` or the
default hub id.
Sampling is model-owned. ``sampling_constraints`` pins ``detokenize`` and the
union of both phase end tokens; ``extra_args`` carry the phase and the
request-local sampling preset, and must be present in the stage's
``default_sampling_params`` so ``has_sampling_extra_args`` turns on and the
per-request args reach the model's forward.
"""
from vllm_omni.config.stage_config import (
PipelineConfig,
StageExecutionType,
StagePipelineConfig,
)
from .yue2 import STOP_TOKEN_IDS
YUE2_PIPELINE = PipelineConfig(
model_type="yue2",
default_deploy_config_name="yue2.yaml",
model_arch="Yue2ForCausalLM",
stages=(
StagePipelineConfig(
stage_id=0,
model_stage="yue2",
execution_type=StageExecutionType.LLM_AR,
input_sources=(),
final_output=True,
final_output_type="audio",
owns_tokenizer=True,
engine_output_type="audio",
# Whole-song audio ships once, on the finishing step; the audio
# accumulator concatenates over time and a single tensor just
# passes through.
sampling_constraints={
"detokenize": False,
"stop_token_ids": list(STOP_TOKEN_IDS),
},
),
),
)
__all__ = ["YUE2_PIPELINE"]
File diff suppressed because it is too large. Load diff