[Fixbug][Perf] Qwen3-omni: code predictor with re-prefill + SDPA and eliminate decode hot-path CPU round-trips (#2012)

Signed-off-by: Junhong Liu <98734602+LJH-LBJ@users.noreply.github.com>
This commit is contained in:
Junhong Liu authored and GitHub committed 2026-03-20 19:33:52 +08:00
1 parent da7a8f8014
commit 6901ba426e
4 files changed
+310 -425

No files matched your search

@@ -159,6 +159,12 @@ class Qwen3OmniMoeForConditionalGeneration(
# suppress tokens by setting their probability to ~1e-9 (finite very small)
self.suppressed_tokens = self._get_talker_suppressed_tokens()
self.requires_raw_input_tokens = True
# Keys that should stay on GPU in model_intermediate_buffer to avoid CPU↔GPU round-trips
self.gpu_resident_buffer_keys: set[str] = {
"last_talker_hidden",
"trailing_text_hidden",
"tts_pad_embed_projected",
}
elif self.model_stage == "code2wav":
self.enable_update_additional_information = True
@@ -223,14 +229,16 @@ class Qwen3OmniMoeForConditionalGeneration(
# ==================== Forward Pass ====================
def _get_talker_suppressed_tokens(self):
return [
i
for i in range(
self.config.talker_config.text_config.vocab_size - 1024,
self.config.talker_config.text_config.vocab_size,
)
if i != self.config.talker_config.codec_eos_token_id
]
"""Return a boolean mask on GPU for suppressed token positions."""
vocab_size = self.config.talker_config.text_config.vocab_size
mask = torch.zeros(vocab_size, dtype=torch.bool)
start = vocab_size - 1024
eos_id = self.config.talker_config.codec_eos_token_id
for i in range(start, vocab_size):
if i != eos_id:
mask[i] = True
# Will be moved to the correct device on first use
return mask
def get_mrope_input_positions(
self,
@@ -578,7 +586,7 @@ class Qwen3OmniMoeForConditionalGeneration(
Postprocess the talker hidden states.
"""
update_dict = {}
update_dict["last_talker_hidden"] = hidden_states[-1, :].detach().to("cpu").contiguous()
update_dict["last_talker_hidden"] = hidden_states[-1, :].detach()
return update_dict
def talker_preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
@@ -632,9 +640,11 @@ class Qwen3OmniMoeForConditionalGeneration(
if inputs_embeds.shape[-1] == 2048:
inputs_embeds = self.text_projection(inputs_embeds)
code_predictor_codes, summed_embeddings = self.talker.code_predictor_forward(
input_ids, inputs_embeds.clone(), last_talker_hidden=last_talker_hidden
input_ids, inputs_embeds, last_talker_hidden=last_talker_hidden
)
inputs_embeds = summed_embeddings.clone()
# summed_embeddings is [B, seq_len, H] (3D) while text_step is [B, H] (2D).
# Flatten to 2D first to avoid wrong broadcasting: [B,1,H]+[B,H] → [B,B,H]
inputs_embeds = summed_embeddings.reshape(-1, self.talker_config.text_config.hidden_size)
inputs_embeds = (inputs_embeds + text_step).reshape(-1, self.talker_config.text_config.hidden_size)
return inputs_embeds, code_predictor_codes.squeeze(-1)
@@ -758,7 +768,7 @@ class Qwen3OmniMoeForConditionalGeneration(
# compatible with old shape [1,S,D]
rem_tail = trailing_text_hidden.squeeze(0)
if rem_tail.shape[0] > 0:
update_dict["trailing_text_hidden"] = rem_tail.detach().to("cpu").contiguous()
update_dict["trailing_text_hidden"] = rem_tail.detach()
# Also persist projected tts_pad for decode fallback if needed
if isinstance(tts_pad_thinker, torch.Tensor):
pad_in = tts_pad_thinker
@@ -767,7 +777,7 @@ class Qwen3OmniMoeForConditionalGeneration(
if pad_in.ndim == 1:
pad_in = pad_in.view(1, 1, -1)
pad_proj = self.talker.text_projection(pad_in.to(self._module_device(self.talker)))
update_dict["tts_pad_embed_projected"] = pad_proj.detach().to("cpu").contiguous()
update_dict["tts_pad_embed_projected"] = pad_proj.detach()
except Exception:
pass
self._talker_cache_thinker_decode_embeds(info_dict, update_dict)
@@ -926,7 +936,7 @@ class Qwen3OmniMoeForConditionalGeneration(
if isinstance(q_tail, torch.Tensor) and q_tail.numel() > 0:
use_vec = q_tail[0:1, :]
new_q_tail = (
q_tail[1:, :].detach().to("cpu").contiguous()
q_tail[1:, :].detach()
if q_tail.shape[0] > 1
else self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
)
@@ -1122,16 +1132,10 @@ class Qwen3OmniMoeForConditionalGeneration(
# implemented by assigning their logits to log(1e-9).
if getattr(self, "model_stage", None) == "talker" and isinstance(logits, torch.Tensor):
try:
logits_cpu = logits.cpu()
logits_cpu[:, self.suppressed_tokens] = -1e9
logits = logits_cpu.to(logits.device)
except Exception as e:
print(f"Error in logits suppression: {e}")
print(f"logits.shape: {logits.shape}")
print(f"self.suppressed_tokens: {self.suppressed_tokens}")
raise e
logits[:, self.suppressed_tokens] = -1e9
# Move mask to device once (lazy), then reuse every step
if self.suppressed_tokens.device != logits.device:
self.suppressed_tokens = self.suppressed_tokens.to(logits.device)
logits.masked_fill_(self.suppressed_tokens.unsqueeze(0), -1e9)
return logits
def sample(
@@ -1,27 +1,16 @@
"""Qwen3-Omni Code Predictor with MTP (Multi-Token Prediction) support.
"""Qwen3-Omni Code Predictor -- optimized re-prefill, no KV cache.
This module implements the code predictor component for Qwen3-Omni talker models.
The code predictor generates residual RVQ (Residual Vector Quantization) codes
autoregressively, predicting layers 1 to N based on layer-0 codes from the talker.
* SDPA attention (F.scaled_dot_product_attention) with native GQA support
* Per-call embedding buffer to avoid cross-request aliasing
* Pre-allocated position_ids (read-only, safe to persist)
* torch.compile on inner transformer by default
* Inline sampling (top-k + top-p) -- no custom op overhead
"""
from collections import namedtuple
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import Cache, PretrainedConfig
from transformers.generation.logits_process import (
LogitsProcessorList,
TopKLogitsWarper,
TopPLogitsWarper,
)
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
from vllm.compilation.decorators import support_torch_compile
from vllm.config import CacheConfig, ModelConfig, VllmConfig, get_current_vllm_config
from vllm.forward_context import get_forward_context
from vllm.config import VllmConfig
from vllm.logger import init_logger
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.linear import (
@@ -32,47 +21,53 @@ from vllm.model_executor.layers.linear import (
from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.layers.rotary_embedding import get_rope
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.utils.torch_utils import direct_register_custom_op
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
# ============================================================================
# Code Predictor Attention Layer
# ============================================================================
# ===================================================================
# Standalone Attention (SDPA, no KV cache, no HF backend fallback)
# ===================================================================
class Qwen3OmniCodePredictorAttention(nn.Module):
"""Multi-head self-attention for code predictor with vLLM optimization."""
"""Multi-head self-attention for code predictor.
Uses ``F.scaled_dot_product_attention`` directly. No KV cache -- the code
predictor always re-prefills the full (short) sequence each AR step.
Input : [B, seq_len, hidden_size]
Output: [B, seq_len, hidden_size]
"""
def __init__(
self,
config,
layer_idx: int,
vllm_config: VllmConfig = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.num_heads = config.code_predictor_config.num_attention_heads
self.num_key_value_heads = config.code_predictor_config.num_key_value_heads
cp_cfg = config.code_predictor_config
self.num_heads = cp_cfg.num_attention_heads
self.num_kv_heads = cp_cfg.num_key_value_heads
self.head_dim = getattr(
config.code_predictor_config,
cp_cfg,
"head_dim",
config.code_predictor_config.hidden_size // config.code_predictor_config.num_attention_heads,
cp_cfg.hidden_size // cp_cfg.num_attention_heads,
)
self.hidden_size = config.code_predictor_config.hidden_size
if self.num_heads % self.num_key_value_heads != 0:
raise ValueError("num_attention_heads must be divisible by num_key_value_heads")
self.num_key_value_groups = self.num_heads // self.num_key_value_heads
self.hidden_size = cp_cfg.hidden_size
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self._use_gqa = self.num_kv_heads != self.num_heads
self.qkv_proj = QKVParallelLinear(
hidden_size=self.hidden_size,
head_size=self.head_dim,
total_num_heads=self.num_heads,
total_num_kv_heads=self.num_key_value_heads,
total_num_kv_heads=self.num_kv_heads,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
@@ -88,120 +83,55 @@ class Qwen3OmniCodePredictorAttention(nn.Module):
)
self.rotary_emb = get_rope(
self.head_dim,
max_position=config.code_predictor_config.max_position_embeddings,
max_position=cp_cfg.max_position_embeddings,
rope_parameters=None,
dual_chunk_attention_config=None,
)
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_key_value_heads * self.head_dim
# Query/Key normalization
self.q_norm = RMSNorm(self.head_dim, eps=config.code_predictor_config.rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=config.code_predictor_config.rms_norm_eps)
self.is_causal = True
self.config = config
self.attention_backends = ["flash_attention_2", "xformers", "eager", "sdpa"]
cudagraph_mode = get_current_vllm_config().compilation_config.cudagraph_mode
if "flash_attention_2" in ALL_ATTENTION_FUNCTIONS and cudagraph_mode.has_full_cudagraphs():
logger.warning(
f"CUDAGraphMode.{cudagraph_mode.name} is currently not supported "
f"with flash attention for Qwen3-Omni talker MTP."
f"removing flash attention from attention_backends"
)
self.attention_backends.remove("flash_attention_2")
self.q_norm = RMSNorm(self.head_dim, eps=cp_cfg.rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=cp_cfg.rms_norm_eps)
def forward(
self,
hidden_states: torch.Tensor,
past_key_values: Cache | None = None,
cache_position: torch.LongTensor | None = None,
use_cache: bool = False,
position_ids: torch.LongTensor | None = None,
position_ids: torch.Tensor,
) -> torch.Tensor:
bsz, seq_len, _ = hidden_states.shape
qkv, _ = self.qkv_proj(hidden_states)
# Flatten to 2-D so vLLM rotary_emb gets [num_tokens, size]
qkv, _ = self.qkv_proj(hidden_states.reshape(bsz * seq_len, -1))
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
# Reshape for attention
q = q.reshape(bsz, seq_len, self.num_heads, self.head_dim)
k = k.reshape(bsz, seq_len, self.num_key_value_heads, self.head_dim)
v = v.reshape(bsz, seq_len, self.num_key_value_heads, self.head_dim)
# Apply normalization
q = self.q_norm(q).contiguous()
k = self.k_norm(k).contiguous()
q = q.reshape(-1, self.q_size)
k = k.reshape(-1, self.kv_size)
# Apply RoPE
# QK-norm -> RoPE (both 2-D)
q = self.q_norm(q.view(-1, self.num_heads, self.head_dim)).view(q.shape)
k = self.k_norm(k.view(-1, self.num_kv_heads, self.head_dim)).view(k.shape)
q, k = self.rotary_emb(position_ids, q, k)
# Reshape for attention
q = q.reshape(bsz, seq_len, self.num_heads, self.head_dim)
k = k.reshape(bsz, seq_len, self.num_key_value_heads, self.head_dim)
# [B, heads, seq, head_dim]
q = q.view(bsz, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(bsz, seq_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
v = v.view(bsz, seq_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
v_heads = v.transpose(1, 2).contiguous()
q_heads = q.transpose(1, 2).contiguous()
k_heads = k.transpose(1, 2).contiguous()
attn_out = F.scaled_dot_product_attention(
q,
k,
v,
scale=self.scaling,
is_causal=True,
enable_gqa=self._use_gqa,
)
if past_key_values is not None:
sin, cos = self.rotary_emb.get_cos_sin(seq_len)
# sin and cos are specific to RoPE models; cache_position needed for the static cache
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
k_heads, v_heads = past_key_values.update(k_heads, v_heads, self.layer_idx, cache_kwargs)
# Try attention backends in order of preference, with runtime error handling
# This handles cases where the backend is registered but not actually available
attn_output = None
last_error = None
for backend_name in self.attention_backends:
if backend_name not in ALL_ATTENTION_FUNCTIONS:
continue
try:
attention_interface = ALL_ATTENTION_FUNCTIONS[backend_name]
attn_output, _ = attention_interface(
self,
q_heads,
k_heads,
v_heads,
None,
dropout=0.0 if not self.training else getattr(self, "attention_dropout", 0.0),
scaling=self.head_dim**-0.5,
sliding_window=None,
use_cache=use_cache,
position_ids=position_ids[:seq_len].unsqueeze(0),
output_hidden_states=True,
output_attentions=False,
)
break
except (ValueError, ImportError, RuntimeError, AttributeError) as e:
# Store error and try next backend
last_error = e
continue
if attn_output is None:
raise RuntimeError(
f"All attention backends failed. Last error: {last_error}. "
"Please install flash-attn, or ensure PyTorch's scaled_dot_product_attention is available."
)
attn_output = attn_output.reshape(*(hidden_states.shape[:-1]), -1).contiguous()
attn_output, _ = self.o_proj(attn_output)
return attn_output
attn_out = attn_out.transpose(1, 2).reshape(bsz * seq_len, -1)
output, _ = self.o_proj(attn_out)
return output.view(bsz, seq_len, -1)
# ============================================================================
# Code Predictor MLP Layer
# ============================================================================
# ===================================================================
# MLP
# ===================================================================
class Qwen3OmniCodePredictorMLP(nn.Module):
"""Feed-forward network for code predictor with fused gate/up projection."""
"""SiLU-gated MLP for code predictor."""
def __init__(
self,
@@ -221,7 +151,6 @@ class Qwen3OmniCodePredictorMLP(nn.Module):
prefix=f"{prefix}.gate_up_proj",
disable_tp=True,
)
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
@@ -238,35 +167,23 @@ class Qwen3OmniCodePredictorMLP(nn.Module):
return down
# ============================================================================
# MTP Layer (Multi-Token Prediction Layer)
# ============================================================================
# ===================================================================
# Decoder Layer
# ===================================================================
class Qwen3OmniCodePredictorMTPLayer(nn.Module):
"""MTP layer for speculative decoding - predicts next residual code layer."""
class Qwen3OmniCodePredictorDecoderLayer(nn.Module):
"""Transformer decoder layer (SDPA, no KV cache)."""
def __init__(
self,
config: PretrainedConfig,
prefix: str,
model_config: ModelConfig,
layer_idx: int,
cache_config: CacheConfig | None = None,
config,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.layer_idx = layer_idx
self.config = config
self.self_attn = Qwen3OmniCodePredictorAttention(
config,
layer_idx,
vllm_config=type(
"VllmConfig",
(),
{"cache_config": cache_config, "quant_config": quant_config, "model_config": model_config},
)(),
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
)
@@ -275,260 +192,258 @@ class Qwen3OmniCodePredictorMTPLayer(nn.Module):
quant_config=quant_config,
prefix=f"{prefix}.mlp",
)
self.input_layernorm = RMSNorm(
config.code_predictor_config.hidden_size, eps=config.code_predictor_config.rms_norm_eps
)
self.post_attention_layernorm = RMSNorm(
config.code_predictor_config.hidden_size, eps=config.code_predictor_config.rms_norm_eps
)
cp_cfg = config.code_predictor_config
self.input_layernorm = RMSNorm(cp_cfg.hidden_size, eps=cp_cfg.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(cp_cfg.hidden_size, eps=cp_cfg.rms_norm_eps)
def mtp_block(
def forward(
self,
hidden_states: torch.Tensor,
past_key_values: Cache | None = None,
cache_position: torch.LongTensor | None = None,
use_cache: bool = False,
position_ids: torch.LongTensor | None = None,
position_ids: torch.Tensor,
) -> torch.Tensor:
# Self-attention with residual
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(hidden_states, past_key_values, cache_position, use_cache, position_ids)
hidden_states = self.self_attn(hidden_states, position_ids)
hidden_states = residual + hidden_states
# MLP with residual
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class Qwen3OmniCodePredictorBaseModel(nn.Module):
"""
Base model for code predictor - matches HF Qwen3OmniMoeTalkerCodePredictorModel structure.
# ===================================================================
# Base Transformer Model (re-prefill, no KV cache)
# ===================================================================
This is a simple transformer that processes inputs_embeds and outputs hidden states.
class Qwen3OmniCodePredictorBaseModel(nn.Module):
"""Inner transformer for code predictor.
Signature: ``forward(inputs_embeds, position_ids) -> hidden_states``
-- plain Tensor in, plain Tensor out (no namedtuple).
"""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config.code_predictor_config
self.config = config
self.vocab_size = config.vocab_size
self.num_code_groups = config.num_code_groups
# Codec embeddings (for layers 1-num_code_groups-1)
self.codec_embedding = nn.ModuleList(
[
VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
)
for _ in range(config.num_code_groups - 1)
]
[VocabParallelEmbedding(config.vocab_size, config.hidden_size) for _ in range(config.num_code_groups - 1)]
)
# Decoder layers
self.layers = nn.ModuleList(
[
Qwen3OmniCodePredictorMTPLayer(
Qwen3OmniCodePredictorDecoderLayer(
vllm_config.model_config.hf_config,
f"{prefix}.layers.{idx}",
model_config=vllm_config.model_config,
layer_idx=idx,
cache_config=vllm_config.cache_config,
quant_config=vllm_config.quant_config,
prefix=f"{prefix}.layers.{idx}",
)
for idx in range(config.num_hidden_layers)
]
)
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
inputs_embeds: torch.Tensor,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: Any | None = None,
use_cache: bool | None = False,
cache_position: torch.LongTensor | None = None,
**kwargs: Any,
) -> Any:
"""
Forward pass matching HF structure.
Args:
inputs_embeds: [batch, seq_len, hidden_size]
position_ids: Optional position IDs tensor
past_key_values: Optional cached key-value pairs
use_cache: Whether to use cache
cache_position: Optional cache position tensor
**kwargs: Additional keyword arguments
Returns:
Named tuple with .last_hidden_state and .past_key_values attributes
"""
batch_size, seq_len, _ = inputs_embeds.shape
# Forward through decoder layers
position_ids: torch.Tensor,
) -> torch.Tensor:
hidden_states = inputs_embeds
for layer in self.layers:
hidden_states = layer.mtp_block(hidden_states, past_key_values, cache_position, use_cache, position_ids)
# Final norm
hidden_states = layer(hidden_states, position_ids)
hidden_states = self.norm(hidden_states)
# Return in HF-compatible format
Output = namedtuple("Output", ["last_hidden_state", "past_key_values"])
return Output(last_hidden_state=hidden_states, past_key_values=None) # [batch, num_code_groups-1, hidden_size]
def get_input_embeddings(self):
"""Return codec embeddings for HF compatibility."""
return self.codec_embedding
return hidden_states
def code_predictor_sample(
logits: torch.Tensor,
layer_name: str,
) -> torch.Tensor:
forward_context = get_forward_context()
self = forward_context.no_compile_layers[layer_name]
logits = self.logits_processors(None, logits[:, -1])
probs = F.softmax(logits, dim=-1)
code = torch.multinomial(probs.squeeze(1), num_samples=1) # [batch, 1]
return code
# ===================================================================
# Code Predictor Wrapper (optimized re-prefill, persistent buffers)
# ===================================================================
def code_predictor_sample_fake(
logits: torch.Tensor,
layer_name: str,
) -> torch.Tensor:
return torch.empty((logits.shape[0], 1), dtype=torch.int64, device=logits.device)
direct_register_custom_op(
op_name="qwen3_omni_code_predictor_sample",
op_func=code_predictor_sample,
fake_impl=code_predictor_sample_fake,
)
@support_torch_compile
class Qwen3OmniMoeTalkerCodePredictor(nn.Module):
"""
Code predictor wrapper matching HF structure.
"""Optimized code predictor -- re-prefill approach, no KV cache.
Structure:
- self.model: Qwen3OmniCodePredictorBaseModel (transformer)
- self.lm_head: ModuleList of output heads
Each AR step forwards the full growing sequence (len 2 -> num_code_groups+1)
through the transformer. The extra O(T^2) FLOPs are negligible for
short sequences, and this avoids all KV-cache management overhead.
Optimizations:
1. Per-call embedding buffer -- avoids cross-request aliasing.
2. Pre-allocated position_ids -- no torch.arange per step.
3. Cached module references -- bypass ModuleList indexing.
4. torch.compile on inner transformer.
5. Inline sampling (top-k + top-p) -- no custom op overhead.
"""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
talker_code_predictor_config = vllm_config.model_config.hf_config
config = vllm_config.model_config.hf_config
self.config = config
self.quant_config = vllm_config.quant_config
self.prefix = prefix
self.config = talker_code_predictor_config
self.vocab_size = self.config.code_predictor_config.vocab_size
self.num_code_groups = self.config.code_predictor_config.num_code_groups
self.num_code_groups = config.code_predictor_config.num_code_groups
self._hidden_size = config.code_predictor_config.hidden_size
# Base transformer model (matches HF structure)
self.model = Qwen3OmniCodePredictorBaseModel(vllm_config=vllm_config, prefix=prefix)
self.model = Qwen3OmniCodePredictorBaseModel(
vllm_config=vllm_config,
prefix=prefix,
)
# Output heads for each residual layer (1-num_layers-1)
# One lm_head per residual layer (layers 1 .. G-1)
self.lm_head = nn.ModuleList(
[
nn.Linear(
self.config.code_predictor_config.hidden_size,
self.config.code_predictor_config.vocab_size,
config.code_predictor_config.hidden_size,
config.code_predictor_config.vocab_size,
bias=False,
)
for _ in range(self.num_code_groups - 1)
]
)
self.logits_processors = LogitsProcessorList(
[
TopKLogitsWarper(top_k=50),
TopPLogitsWarper(top_p=0.8),
]
)
compilation_config = get_current_vllm_config().compilation_config
if prefix in compilation_config.static_forward_context:
raise ValueError(f"Duplicate layer name: {prefix}")
compilation_config.static_forward_context[prefix] = self
self.layer_name = prefix
self.set_sampling_params()
# Lazily initialised position ids (read-only, safe to persist)
self._pos_ids: torch.Tensor | None = None
# Cached plain-list refs (set once)
self._lm_heads: list | None = None
self._codec_embeds: list | None = None
# Model forward (optionally compiled)
self._model_fwd: object | None = None
def set_sampling_params(self, top_k: int = 50, top_p: float = 0.8):
"""Configure sampling parameters to maintain consistency with previous implementation."""
self._top_k = top_k
self._top_p = top_p
logger.debug(f"Sampling parameters updated: top_k={top_k}, top_p={top_p}s")
# ------------------------------------------------------------------
# Lazy-init helpers
# ------------------------------------------------------------------
def _ensure_pos_ids(self, device: torch.device) -> None:
if self._pos_ids is not None and self._pos_ids.device == device:
return
max_seq = self.num_code_groups + 1
self._pos_ids = torch.arange(max_seq, dtype=torch.long, device=device)
def _ensure_cached_refs(self) -> None:
if self._lm_heads is not None:
return
self._lm_heads = list(self.lm_head)
self._codec_embeds = list(self.model.codec_embedding)
def _ensure_model_fwd(self) -> None:
if self._model_fwd is not None:
return
if current_omni_platform.supports_torch_inductor():
# torch.compile fuses the 5-layer transformer's small kernels,
# reducing BF16 intermediate round-trips and improving precision.
# mode="default" avoids Inductor's own CUDA graph capture so it
# doesn't conflict with the outer CUDAGraphWrapper.
self._model_fwd = torch.compile(
self.model.forward,
mode="default",
dynamic=True,
)
logger.info("code_predictor: torch.compile enabled (mode=default)")
else:
self._model_fwd = self.model.forward
logger.info("code_predictor: using eager mode (no torch.compile)")
# ------------------------------------------------------------------
# Forward -- re-prefill + inline sampling
# ------------------------------------------------------------------
@torch.inference_mode()
def forward(
self,
layer0_code: torch.Tensor,
layer0_embed: torch.Tensor,
last_talker_hidden: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Forward pass for code predictor.
"""Predict residual codebooks 1..G-1 autoregressively via re-prefill.
Args:
layer0_code:
Code index for code-group (layer) 0.
Shape: [batch_size, 1], dtype typically int64.
last_talker_hidden:
Shape: [batch_size, hidden_size].
layer0_code: [bsz, 1] int64
layer0_embed: [bsz, 1, hidden_size]
last_talker_hidden: [bsz, 1, hidden_size]
Returns:
pos_all_layers:
Predicted codes for all code groups, including `layer0_code`.
Shape: [batch_size, num_code_groups, 1].
current_input:
The final input embedding sequence after appending embeddings of all
predicted codes (one token per predicted layer).
Shape: [batch_size, num_code_groups + 2, hidden_size].
all_codes: [bsz, num_code_groups, 1]
proj_buf: [bsz, num_code_groups + 1, hidden_size]
pos 0 = last_talker_hidden (NOT a codec embed)
pos 1 = layer0_embed
pos 2.. = codec_embedding[i](predicted_code_i)
"""
pos_codes = [layer0_code] # Start with layer 0: [batch, 1]
try:
current_input = torch.cat([last_talker_hidden, layer0_embed], dim=1) # [batch, 2, hidden_size]
except Exception as e:
print(f"Error in current_input: {e}")
print(f"last_talker_hidden shape: {last_talker_hidden.shape}")
print(f"prev_embed shape: {layer0_embed.shape}")
raise e
batch_size = current_input.shape[0]
bsz = int(layer0_code.shape[0])
device = layer0_code.device
dtype = last_talker_hidden.dtype
num_groups = self.num_code_groups
# Predict all residual layers (layers 1 to num_code_groups-1) autoregressively
for layer_idx in range(self.num_code_groups - 1):
seq_len = layer_idx + 2
# Compute position_ids dynamically to avoid torch.compile specializing batch_size
position_ids = torch.arange(seq_len, device=current_input.device, dtype=torch.int64).repeat(batch_size)
# Forward through code_predictor model
outputs = self.model(
inputs_embeds=current_input,
attention_mask=None,
position_ids=position_ids,
past_key_values=None,
use_cache=False,
cache_position=None,
)
hidden_state = outputs.last_hidden_state # [batch, 2, hidden_size]
# Lazy init (read-only caches only)
self._ensure_pos_ids(device)
self._ensure_model_fwd()
self._ensure_cached_refs()
# Use the corresponding lm_head for this layer
logits = self.lm_head[layer_idx](hidden_state[:, -1:, :])
code = torch.ops.vllm.qwen3_omni_code_predictor_sample(logits, self.layer_name)
pos_codes.append(code)
# Update prev_embed for next layer (if not last layer)
# layer_idx=0 predicts layer 1, embed with codec_embedding[1]
new_embed = self.model.codec_embedding[layer_idx](code) # [batch, 1, hidden_size]
current_input = torch.cat([current_input, new_embed], dim=1) # [batch, 3~n, hidden_size]
pos_all_layers = torch.stack(pos_codes, dim=1) # [batch, num_code_groups, 1]
return pos_all_layers, current_input
# Allocate proj_buf locally each call to avoid cross-call aliasing
max_seq = num_groups + 1
proj_buf = torch.zeros(bsz, max_seq, self._hidden_size, dtype=dtype, device=device)
pos_ids = self._pos_ids
model_fwd = self._model_fwd
lm_heads = self._lm_heads
codec_embeds = self._codec_embeds
# Output codes
all_codes = torch.empty(bsz, num_groups, 1, dtype=torch.int64, device=device)
all_codes[:, 0] = layer0_code
# Fill buffer positions 0 & 1
proj_buf[:bsz, 0:1, :] = last_talker_hidden
proj_buf[:bsz, 1:2, :] = layer0_embed
# Autoregressive loop: predict layers 1..G-1
for step in range(1, num_groups):
seq_len = step + 1
projected = proj_buf[:bsz, :seq_len, :]
step_pos_ids = pos_ids[:seq_len] if bsz == 1 else pos_ids[:seq_len].repeat(bsz)
assert step_pos_ids.shape[0] == bsz * seq_len
hidden_out = model_fwd(projected, step_pos_ids)
# Inline sampling: top-k -> top-p -> softmax -> multinomial
logits = lm_heads[step - 1](hidden_out[:, -1, :]) # [bsz, vocab]
if self._top_k > 0:
topk_vals, _ = logits.topk(self._top_k, dim=-1)
logits = logits.masked_fill(logits < topk_vals[:, -1:], float("-inf"))
if self._top_p < 1.0:
sorted_logits, sorted_idx = logits.sort(dim=-1, descending=True)
cumulative_probs = F.softmax(sorted_logits, dim=-1).cumsum(dim=-1)
# Remove tokens with cumulative probability above top_p
remove_mask = cumulative_probs - F.softmax(sorted_logits, dim=-1) >= self._top_p
sorted_logits[remove_mask] = float("-inf")
logits = sorted_logits.scatter(1, sorted_idx, sorted_logits)
probs = F.softmax(logits, dim=-1)
code = torch.multinomial(probs, num_samples=1) # [bsz, 1]
all_codes[:, step] = code
# Embed predicted code -> next buffer position
new_embed = codec_embeds[step - 1](code) # [batch, 1, hidden_size]
proj_buf[:bsz, step + 1 : step + 2, :] = new_embed
return all_codes, proj_buf[:bsz]
# ------------------------------------------------------------------
# Weight loading
# ------------------------------------------------------------------
def load_weights(self, weights: list[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights with mapping for fused QKV and gate_up projections.
@@ -129,111 +129,70 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
self.code_predictor = Qwen3OmniMoeTalkerCodePredictor(
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "code_predictor")
)
max_batch_size = max(
vllm_config.scheduler_config.max_num_seqs, vllm_config.compilation_config.max_cudagraph_capture_size
)
self.layer0_embed_buffer = torch.zeros(
(max_batch_size, 1, self.config.text_config.hidden_size),
dtype=vllm_config.model_config.dtype,
)
def code_predictor_forward(
self,
input_ids: torch.Tensor,
inputs_embeds: torch.Tensor | None = None,
*,
temperature: float = 1.0,
top_k: int = 50, # Match transformers default
top_p: float = 0.8, # Match transformers default
generation_steps: int | None = None,
last_talker_hidden: torch.Tensor | None = None,
**_: object,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Generate full RVQ codec codes for the provided sequence.
"""Generate full RVQ codes + summed embeddings (single-loop, no KV cache).
The code predictor consumes the layer-0 codec codes produced by the talker
alongside the talker's hidden states, and autoregressively predicts the remaining
residual layers (to num_codec_groups).
The code predictor uses re-prefill: each AR step re-forwards the full
(short) sequence through the transformer. The returned ``proj_buf``
already contains all codec embeddings at positions 1..G,
so summed_embeddings = proj_buf[:, 1:, :].sum(dim=1) — no second
loop or re-embedding needed.
Returns:
tuple containing:
- residual_codes: A tensor of shape [batch, num_code_groups, seq_len] containing
the complete set of codec codes
- summed_embeddings: A tensor of shape [batch, seq_len, hidden_size]
Sum of all layer embeddings at each position (like Transformers)
result_codes: [batch, num_code_groups, seq_len]
summed_embeddings: [batch, seq_len, hidden_size]
"""
if input_ids is None:
raise ValueError("`input_ids` containing layer-0 codec codes must be provided.")
raise ValueError("`input_ids` (layer-0 codes) must be provided.")
if inputs_embeds is None:
raise ValueError("`inputs_embeds` containing talker hidden states must be provided.")
raise ValueError("`inputs_embeds` (talker hidden states) must be provided.")
if inputs_embeds.ndim == 2:
inputs_embeds = inputs_embeds.unsqueeze(0)
if input_ids.ndim == 1:
input_ids = input_ids.unsqueeze(0)
# Ensure the tensors are contiguous for the autoregressive sampling loop
inputs_embeds = inputs_embeds.contiguous()
input_ids = input_ids.contiguous()
# Generate full codec codes using MTP
# This will be the parallel prediction implementation
batch_size, seq_len = input_ids.shape
device = input_ids.device
embed_fn = self.language_model.model.codec_embedding
hidden_size = self.config.code_predictor_config.hidden_size
# For now, use sequential generation (TODO: implement parallel)
# Result will be [batch, num_code_groups, seq_len]
# - all_codes_per_position will collect [batch, num_code_groups, 1] for each position
all_codes_per_position = []
middle_hidden_states = [] # Collect hidden states for each position
# Generate residual layers for each position
for pos in range(seq_len):
layer0_code = input_ids[:, pos : pos + 1] # [batch, 1]
# Initial input: [last_talker_hidden, layer0_embed]
layer0_embed = self.embed_input_ids(layer0_code)
self.layer0_embed_buffer[:batch_size].copy_(layer0_embed)
pos_all_layers, current_input = self.code_predictor(
layer0_code, self.layer0_embed_buffer[:batch_size], last_talker_hidden
)
# Stack all layers for this position: [batch, num_code_groups, 1]
all_codes_per_position.append(pos_all_layers)
middle_hidden_states.append(current_input[:, 2:-1, :])
# Concatenate across positions: [batch, num_code_groups, seq_len]
result_codes = torch.cat(all_codes_per_position, dim=2)
# Build summed embeddings for each position (like Transformers)
# This combines layer-0 embed, mid layers hidden states, and last layer embed
all_summed_embeddings = []
result_codes = torch.empty(
batch_size,
self.num_code_groups,
seq_len,
dtype=torch.int64,
device=device,
)
summed_embeddings = torch.empty(
batch_size,
seq_len,
hidden_size,
dtype=inputs_embeds.dtype,
device=device,
)
for pos in range(seq_len):
# Layer 0 embedding
layer0_code = result_codes[:, 0, pos : pos + 1] # [batch, 1]
layer0_embed = self.embed_input_ids(layer0_code) # [batch, 1, hidden_size]
layer0_code = input_ids[:, pos : pos + 1]
layer0_embed = embed_fn(layer0_code)
# mid layers hidden states (from CodePredictor)
mid_residual_hiddens = middle_hidden_states[pos] # [batch, num_code_groups-2, hidden_size]
mid_list = list(mid_residual_hiddens.split(1, dim=1))
# last layer embedding
last_layer_code = result_codes[:, -1, pos : pos + 1] # [batch, 1]
last_residual_hidden = self.code_predictor.model.codec_embedding[-1](last_layer_code)
# Concatenate all layers: [batch, num_code_groups, hidden_size]
pos_codec_hiddens = torch.cat(
[layer0_embed] + mid_list + [last_residual_hidden],
dim=1,
pos_all_layers, proj_buf = self.code_predictor(
layer0_code,
layer0_embed,
last_talker_hidden,
)
# Sum across layers: [batch, 1, hidden_size] (like Transformers)
pos_summed = pos_codec_hiddens.sum(dim=1, keepdim=True)
all_summed_embeddings.append(pos_summed)
# Concatenate across positions: [batch, seq_len, hidden_size]
summed_embeddings = torch.cat(all_summed_embeddings, dim=1).squeeze(1)
result_codes[:, :, pos : pos + 1] = pos_all_layers
# proj_buf layout: [0]=talker_hidden, [1..G]=codec embeds
summed_embeddings[:, pos, :] = proj_buf[:, 1:, :].sum(dim=1)
return result_codes, summed_embeddings
+8 -1
View File
@@ -1362,10 +1362,17 @@ class OmniGPUModelRunner(GPUModelRunner):
req_state = self.requests.get(req_id)
if req_state is None:
return
# Check if the model declares keys that should stay on GPU
gpu_keys: set[str] = set()
if hasattr(self, "model") and hasattr(self.model, "gpu_resident_buffer_keys"):
gpu_keys = self.model.gpu_resident_buffer_keys
existing = self.model_intermediate_buffer.setdefault(req_id, {})
for k, v in upd.items():
if isinstance(v, torch.Tensor):
existing[k] = v.detach().to("cpu").contiguous()
if k in gpu_keys:
existing[k] = v.detach().clone()
else:
existing[k] = v.detach().to("cpu").contiguous()
elif isinstance(v, list):
existing[k] = [
(item.detach().to("cpu").contiguous() if isinstance(item, torch.Tensor) else item) for item in v