[Model] Aura support - Non async chunk path (#4257)

Signed-off-by: Rein Yang <ruiruyang2@gmail.com>
Signed-off-by: R2-Y <ruiruyang2@gmail.com>
This commit is contained in:
rein yang authored and GitHub committed 2026-06-21 22:08:26 +08:00
1 parent 1292c47804
commit afbcc51672
30 files changed
+2958 -26

No files matched your search

@@ -0,0 +1,84 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""AURA Omni pipeline topology.
Semantic modules:
1. Qwen3-ASR: microphone audio -> transcript text
2. AURA/Qwen3-VL: transcript + video -> text or <|silent|>
3. Qwen3-TTS: response text -> audio
The Qwen3-TTS module is represented as two native engine stages, Talker and
Code2Wav, to reuse the existing vLLM-Omni implementation without wrapping or
duplicating codec streaming behavior.
"""
from vllm_omni.config.stage_config import (
PipelineConfig,
StageExecutionType,
StagePipelineConfig,
)
_AURA_PROC = "vllm_omni.model_executor.stage_input_processors.aura_omni"
_QWEN3_TTS_PROC = "vllm_omni.model_executor.stage_input_processors.qwen3_tts"
AURA_OMNI_PIPELINE = PipelineConfig(
model_type="aura_omni",
model_arch="Qwen3ASRForConditionalGeneration",
stages=(
StagePipelineConfig(
stage_id=0,
model_stage="asr",
execution_type=StageExecutionType.LLM_AR,
input_sources=(),
owns_tokenizer=True,
requires_multimodal_data=True,
engine_output_type="text",
model_arch="Qwen3ASRForConditionalGeneration",
sampling_constraints={"detokenize": True},
),
StagePipelineConfig(
stage_id=1,
model_stage="aura",
execution_type=StageExecutionType.LLM_AR,
input_sources=(0,),
final_output=True,
final_output_type="text",
owns_tokenizer=True,
requires_multimodal_data=True,
engine_output_type="text",
model_arch="AuraQwen3VLForConditionalGeneration",
custom_process_input_func=f"{_AURA_PROC}.asr2aura",
sampling_constraints={"detokenize": True},
),
StagePipelineConfig(
stage_id=2,
model_stage="qwen3_tts",
execution_type=StageExecutionType.LLM_AR,
input_sources=(1,),
owns_tokenizer=True,
engine_output_type="latent",
model_arch="Qwen3TTSTalkerForConditionalGeneration",
custom_process_input_func=f"{_AURA_PROC}.aura2tts",
custom_process_next_stage_input_func=f"{_QWEN3_TTS_PROC}.talker2code2wav_full_payload",
async_chunk_process_next_stage_input_func=f"{_QWEN3_TTS_PROC}.talker2code2wav_async_chunk",
sampling_constraints={
"detokenize": False,
"stop_token_ids": [2150],
},
),
StagePipelineConfig(
stage_id=3,
model_stage="code2wav",
execution_type=StageExecutionType.LLM_GENERATION,
input_sources=(2,),
final_output=True,
final_output_type="audio",
engine_output_type="audio",
model_arch="Qwen3TTSCode2Wav",
sync_process_input_func=f"{_QWEN3_TTS_PROC}.talker2code2wav_token_only",
sampling_constraints={"detokenize": True},
extras={"tts_args": {"max_instructions_length": 500}},
),
),
)
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""AURA-compatible Qwen3-VL wrapper.
AURA ships Qwen3-VL weights with a repository-local ``configuration_qwen3_vl``
module in ``auto_map``. With ``trust_remote_code=True``, Transformers therefore
returns a remote ``Qwen3VLConfig`` class. The model structure is still Qwen3-VL,
but vLLM's stock multimodal processor checks for the upstream Transformers
``Qwen3VLConfig`` type exactly. This wrapper keeps the stock model and processor
logic, while accepting structurally-compatible remote Qwen3-VL configs.
"""
from __future__ import annotations
from typing import Any
from vllm.model_executor.models.qwen3_vl import (
Qwen3VLConfig,
Qwen3VLDummyInputsBuilder,
Qwen3VLForConditionalGeneration,
Qwen3VLMultiModalProcessor,
Qwen3VLProcessingInfo,
Qwen3VLProcessor,
)
from vllm.multimodal import MULTIMODAL_REGISTRY
class AuraQwen3VLProcessingInfo(Qwen3VLProcessingInfo):
def get_hf_config(self) -> Any:
try:
return self.ctx.get_hf_config(Qwen3VLConfig)
except TypeError:
config = self.ctx.get_hf_config()
if (
getattr(config, "model_type", None) == "qwen3_vl"
and hasattr(config, "vision_config")
and hasattr(config, "text_config")
):
return config
raise
def get_hf_processor(self, **kwargs: object) -> Any:
# AURA's config advertises `processing_qwen3_vl.py` in auto_map, but
# the checkpoint snapshot does not include that file. Force the
# upstream processor class while still allowing AURA's remote config.
return self.ctx.get_hf_processor(
Qwen3VLProcessor,
use_fast=kwargs.pop("use_fast", True),
**kwargs,
)
@MULTIMODAL_REGISTRY.register_processor(
Qwen3VLMultiModalProcessor,
info=AuraQwen3VLProcessingInfo,
dummy_inputs=Qwen3VLDummyInputsBuilder,
)
class AuraQwen3VLForConditionalGeneration(Qwen3VLForConditionalGeneration):
pass
@@ -30,7 +30,7 @@ from vllm_omni.utils.speaker_cache import (
)
from .configuration_qwen3_tts import Qwen3TTSConfig, Qwen3TTSSpeakerEncoderConfig, Qwen3TTSTalkerConfig
from .prompt_embeds_builder import Qwen3TTSPromptEmbedsBuilder
from .prompt_embeds_builder import PRECOMPUTED_TEXT_IDS_KEY, Qwen3TTSPromptEmbedsBuilder
from .qwen3_tts_code_predictor_vllm import Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM
from .tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Config
from .tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Encoder
@@ -40,6 +40,19 @@ logger = init_logger(__name__)
_TRAILING_TEXT_COMPACT_MIN_FRAMES = 64
def _has_tts_text_conditioning(info_dict: dict[str, Any], hidden_states: Any | None = None) -> bool:
text_list = info_dict.get("text")
if isinstance(text_list, list) and bool(text_list) and bool(text_list[0]):
return True
if PRECOMPUTED_TEXT_IDS_KEY in info_dict:
return True
if isinstance(hidden_states, dict):
tail = hidden_states.get("trailing_text")
if isinstance(tail, torch.Tensor):
return True
return False
# ---------------------------------------------------------------------------
# Components ported from the HuggingFace Qwen3-TTS reference implementation.
# Only the classes actually needed by the vLLM AR Talker are kept here.
@@ -625,9 +638,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
except Exception:
is_prefill = span_len > 1
text_list = info_dict.get("text")
if not isinstance(text_list, list) or not text_list or not text_list[0]:
raise ValueError("Missing additional_information.text for Qwen3-TTS AR talker.")
if not _has_tts_text_conditioning(info_dict, hs):
raise ValueError("Missing Qwen3-TTS text conditioning: provide `text` or precomputed text token ids.")
task_type = (info_dict.get("task_type") or ["CustomVoice"])[0]
codec_streaming_val = meta.get("codec_streaming")
@@ -650,7 +662,6 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
if is_prefill:
# Prefill (prompt embeddings)
prompt_embeds_cpu = embed.get("prefill")
# First prefill round: prompt_embeds_cpu is not yet populated.
# Subsequent prefill rounds (multi-chunk): prompt_embeds_cpu is a Tensor stored by the first round.
is_first_prefill = not isinstance(prompt_embeds_cpu, torch.Tensor) or prompt_embeds_cpu.ndim != 2
@@ -808,9 +819,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
hs = payload.get("hidden_states", {})
meta = payload.get("meta", {})
text_list = info_dict.get("text")
if not isinstance(text_list, list) or not text_list or not text_list[0]:
raise ValueError("Missing additional_information.text for Qwen3-TTS AR talker.")
if not _has_tts_text_conditioning(info_dict, hs):
raise ValueError("Missing Qwen3-TTS text conditioning: provide `text` or precomputed text token ids.")
task_type = (info_dict.get("task_type") or ["CustomVoice"])[0]
codec_streaming_val = meta.get("codec_streaming")
@@ -343,6 +343,11 @@ _OMNI_MODELS = {
"minicpmo_4_5_omni_tts",
"MiniCPMO45OmniTTSForConditionalGeneration",
),
"AuraQwen3VLForConditionalGeneration": (
"aura_omni",
"qwen3_vl",
"AuraQwen3VLForConditionalGeneration",
),
}
@@ -0,0 +1,390 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Stage processors for the AURA Omni pipeline."""
from __future__ import annotations
import math
import os
from pathlib import Path
from typing import Any
import soundfile as sf
from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.model_executor.models.qwen3_tts.prompt_embeds_builder import (
PRECOMPUTED_TEXT_IDS_KEY,
)
DEFAULT_AURA_SYSTEM_PROMPT = (
"You are receiving a live video stream where the final frame is the present moment. "
"Respond only when a response is needed based on the user's message or the visual context. "
"Otherwise, output '<|silent|>' to signify silence. Respond in Chinese."
)
SILENT_TEXT = "<|silent|>"
QWEN_IM_START_ID = 151644
QWEN_IM_END_ID = 151645
QWEN_ASSISTANT_ID = 77091
QWEN_NEWLINE_ID = 198
QWEN_ASSISTANT_PREFIX_IDS = [QWEN_IM_START_ID, QWEN_ASSISTANT_ID, QWEN_NEWLINE_ID]
QWEN_ASSISTANT_SUFFIX_IDS = [
QWEN_IM_END_ID,
QWEN_NEWLINE_ID,
QWEN_IM_START_ID,
QWEN_ASSISTANT_ID,
QWEN_NEWLINE_ID,
]
DEFAULT_QWEN3_TTS_REF_AUDIO = "vllm-omni/tests/assets/qwen3_tts/clone_2.wav"
DEFAULT_QWEN3_TTS_REF_TEXT = (
"Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
)
def default_qwen3_tts_ref_audio_path() -> str:
"""Return absolute path to the bundled ``clone_2.wav`` reference asset."""
bundled = Path(__file__).resolve().parents[3] / "tests" / "assets" / "qwen3_tts" / "clone_2.wav"
if bundled.is_file():
return str(bundled)
return DEFAULT_QWEN3_TTS_REF_AUDIO
def _as_list(value: Any) -> list[Any]:
if value is None:
return []
return value if isinstance(value, list) else [value]
def _as_prompt_dict(prompt_item: Any) -> dict[str, Any]:
return prompt_item if isinstance(prompt_item, dict) else {}
def _first_value(value: Any, default: Any = None) -> Any:
if isinstance(value, list):
return value[0] if value else default
return default if value is None else value
def _first_bool(value: Any, default: bool = False) -> bool:
value = _first_value(value, default)
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
def _normalize_qwen3_tts_speaker(speaker: Any) -> Any:
if not isinstance(speaker, str):
return speaker
speaker = speaker.strip()
if not speaker:
return speaker
if "_" in speaker:
return speaker
return speaker[0].upper() + speaker[1:].lower()
def _extract_output(source_output: Any) -> Any:
outputs = getattr(source_output, "outputs", None)
if isinstance(outputs, list) and outputs:
return outputs[0]
return source_output
def _extract_text(source_output: Any) -> str:
output = _extract_output(source_output)
cumulative_text = getattr(output, "cumulative_text", None)
if isinstance(cumulative_text, str) and cumulative_text:
return cumulative_text
text = getattr(output, "text", None)
if isinstance(text, str):
return text
mm = getattr(output, "multimodal_output", None)
if isinstance(mm, dict):
for key in ("text", "transcript", "asr_text"):
value = mm.get(key)
if isinstance(value, str):
return value
if isinstance(value, list) and value and isinstance(value[0], str):
return value[0]
return ""
def _extract_token_ids(source_output: Any) -> list[int]:
output = _extract_output(source_output)
token_ids = getattr(output, "cumulative_token_ids", None)
if isinstance(token_ids, list):
return [int(token_id) for token_id in token_ids if isinstance(token_id, int)]
return []
def _trim_aura_response_token_ids(token_ids: list[int]) -> list[int]:
ids = list(token_ids)
if ids[: len(QWEN_ASSISTANT_PREFIX_IDS)] == QWEN_ASSISTANT_PREFIX_IDS:
ids = ids[len(QWEN_ASSISTANT_PREFIX_IDS) :]
if QWEN_IM_END_ID in ids:
ids = ids[: ids.index(QWEN_IM_END_ID)]
while ids and ids[-1] in {QWEN_IM_START_ID, QWEN_IM_END_ID, QWEN_NEWLINE_ID}:
ids.pop()
return ids
def _qwen3_tts_assistant_token_ids_from_aura(source_output: Any) -> list[int]:
content_ids = _trim_aura_response_token_ids(_extract_token_ids(source_output))
if not content_ids:
return []
return QWEN_ASSISTANT_PREFIX_IDS + content_ids + QWEN_ASSISTANT_SUFFIX_IDS
def _source_prompt_by_request_id(source_outputs: list[Any], prompt: Any) -> dict[str, dict[str, Any]]:
prompts = _as_list(prompt)
return {
str(getattr(source_output, "request_id", idx)): _as_prompt_dict(prompt_item)
for idx, (source_output, prompt_item) in enumerate(zip(source_outputs, prompts))
}
def _vision_placeholder(multi_modal_data: dict[str, Any]) -> str:
if "video" in multi_modal_data:
return "<|vision_start|><|video_pad|><|vision_end|>"
if "image" in multi_modal_data:
return "<|vision_start|><|image_pad|><|vision_end|>"
return ""
def _vision_multimodal_data(multi_modal_data: dict[str, Any]) -> dict[str, Any]:
return {key: value for key, value in multi_modal_data.items() if key in {"image", "video"}}
def _aura_prompt(system_prompt: str, transcript: str, multi_modal_data: dict[str, Any]) -> str:
vision = _vision_placeholder(multi_modal_data)
query = transcript.strip()
user_body = f"{vision}{query}" if query else vision
return (
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
f"<|im_start|>user\n{user_body}<|im_end|>\n"
"<|im_start|>assistant\n"
)
def asr2aura(
source_outputs: list[Any],
prompt: Any = None,
requires_multimodal_data: bool = True,
) -> list[dict[str, Any]]:
"""Build AURA Qwen3-VL prompts from ASR transcripts and original video payloads."""
prompt_by_request_id = _source_prompt_by_request_id(source_outputs, prompt)
next_inputs: list[dict[str, Any]] = []
for idx, source_output in enumerate(source_outputs):
src_prompt = prompt_by_request_id.get(str(getattr(source_output, "request_id", idx)), {})
additional_info = src_prompt.get("additional_information") or {}
system_prompt = _first_value(additional_info.get("aura_system_prompt"), DEFAULT_AURA_SYSTEM_PROMPT)
transcript = _extract_text(source_output)
multi_modal_data = {}
source_multi_modal_data = src_prompt.get("multi_modal_data") or {}
if isinstance(source_multi_modal_data, dict):
multi_modal_data.update(source_multi_modal_data)
deferred_multi_modal_data = additional_info.get("deferred_multi_modal_data") or {}
if isinstance(deferred_multi_modal_data, dict):
multi_modal_data.update(deferred_multi_modal_data)
multi_modal_data = _vision_multimodal_data(multi_modal_data)
next_input: dict[str, Any] = {
"prompt": _aura_prompt(str(system_prompt), transcript, multi_modal_data),
}
if requires_multimodal_data:
next_input["multi_modal_data"] = multi_modal_data
if src_prompt.get("mm_processor_kwargs") is not None:
next_input["mm_processor_kwargs"] = src_prompt.get("mm_processor_kwargs")
next_inputs.append(next_input)
return next_inputs
def _estimate_ref_code_len_from_ref_audio(ref_audio: Any) -> int | None:
"""Estimate Qwen3-TTS ref_code length from a ref-audio payload.
For Qwen3-TTS 12Hz models, code length is approximately:
ceil(duration_seconds * 12.5)
i.e. one codec frame per 1920 samples at 24kHz.
"""
codec_frame_rate = 24000.0 / 1920.0
# Unwrap common list wrappers.
item = ref_audio
while isinstance(item, list) and item:
item = item[0]
# Accept tuple/list like (wav, sr).
if isinstance(item, (tuple, list)) and len(item) == 2 and isinstance(item[1], (int, float)):
wav, sr = item
sr_i = int(sr)
if sr_i <= 0:
return None
if hasattr(wav, "__len__"):
n_samples = len(wav)
elif hasattr(wav, "shape"):
shape = getattr(wav, "shape", None)
if not shape:
return None
n_samples = shape[-1] if len(shape) > 1 else shape[0]
else:
return None
if n_samples <= 0:
return None
return max(1, int(math.ceil((float(n_samples) / float(sr_i)) * codec_frame_rate)))
# Accept file path (wav only).
if isinstance(item, str) and item:
audio_path = item
if not os.path.isfile(audio_path) or not audio_path.lower().endswith(".wav"):
return None
try:
info = sf.info(audio_path)
n_frames = int(info.frames)
sr = int(info.samplerate)
if n_frames <= 0 or sr <= 0:
return None
return max(1, int(math.ceil((float(n_frames) / float(sr)) * codec_frame_rate)))
except Exception:
return None
return None
def _estimate_tts_prompt_len_from_token_ids(
token_ids: list[int],
*,
task_type: str = "Base",
language: str = "Chinese",
instruct: str = "",
x_vector_only_mode: bool = False,
non_streaming_mode: bool | None = None,
ref_code_len: int | None = None,
) -> int:
"""Estimate Talker prefill length from prompt structure.
This mirrors Qwen3-TTS prompt assembly at length level:
prompt_len = instruct_len + role_len + codec_prefix_len + text/icl term
"""
# Official defaults: Base -> streaming, others -> non-streaming.
if non_streaming_mode is None:
non_streaming_mode = task_type in ("CustomVoice", "VoiceDesign")
# We do not have tokenizer here; use char length as a monotonic proxy.
instruct_len = len(instruct.strip()) if isinstance(instruct, str) else 0
assistant_len = max(0, len(token_ids))
# role_len = 3; codec_prefix_len = (prefill_len + speaker_len + 2) - 1
# prefill_len = 4 when language_id exists else 3. Use non-auto language as
# the language-id-present proxy.
has_language_id = isinstance(language, str) and language.strip().lower() != "auto"
prefill_len = 4 if has_language_id else 3
speaker_len = 1 if task_type in ("CustomVoice", "Base") else 0
base_len = instruct_len + 3 + (prefill_len + speaker_len + 2 - 1)
if task_type in ("CustomVoice", "VoiceDesign"):
if non_streaming_mode:
prompt_len = base_len + max(0, assistant_len - 6)
else:
prompt_len = base_len + 1
return max(32, min(4096, int(prompt_len)))
if task_type == "Base":
in_context_mode = not bool(x_vector_only_mode)
if in_context_mode and ref_code_len is not None:
codec_lens = 1 + int(ref_code_len)
if non_streaming_mode:
# Exact non-streaming ICL needs ref_ids token length; unavailable
# in this processor. Keep a conservative upper estimate.
prompt_len = base_len + codec_lens + max(0, assistant_len - 8) + 1
else:
# Streaming ICL exact length term: 1 + ref_code_len
prompt_len = base_len + codec_lens
else:
# Base x-vector-only (or missing ref_code length) follows CV shape.
if non_streaming_mode:
prompt_len = base_len + max(0, assistant_len - 6)
else:
prompt_len = base_len + 1
return max(32, min(4096, int(prompt_len)))
# Defensive fallback for unknown task types.
return max(32, min(4096, int(base_len + max(assistant_len, 1))))
def aura2tts(
source_outputs: list[Any],
prompt: Any = None,
requires_multimodal_data: bool = False,
) -> list[OmniTokensPrompt]:
"""Convert AURA text output into Qwen3-TTS Talker requests."""
del requires_multimodal_data
prompt_by_request_id = _source_prompt_by_request_id(source_outputs, prompt)
next_inputs: list[OmniTokensPrompt] = []
for idx, source_output in enumerate(source_outputs):
text = _extract_text(source_output).strip()
if not text or text == SILENT_TEXT:
continue
src_prompt = prompt_by_request_id.get(str(getattr(source_output, "request_id", idx)), {})
additional_info = src_prompt.get("additional_information") or {}
task_type = _first_value(additional_info.get("tts_task_type"), "Base")
language = _first_value(additional_info.get("tts_language"), "English")
instruct = _first_value(additional_info.get("tts_instruct"), "")
x_vector_only_mode = _first_bool(additional_info.get("tts_x_vector_only_mode"), False)
non_streaming_mode_raw = _first_value(additional_info.get("tts_non_streaming_mode"), None)
non_streaming_mode = non_streaming_mode_raw if isinstance(non_streaming_mode_raw, bool) else None
ref_code_len_raw = _first_value(additional_info.get("tts_ref_code_length"), None)
ref_code_len = int(ref_code_len_raw) if isinstance(ref_code_len_raw, int) else None
ref_audio = None
ref_text = None
if task_type == "Base" and not x_vector_only_mode and ref_code_len is None:
ref_audio = _first_value(additional_info.get("tts_ref_audio"), None)
ref_code_len = _estimate_ref_code_len_from_ref_audio(ref_audio)
assistant_token_ids_for_len = _qwen3_tts_assistant_token_ids_from_aura(source_output)
pass_token_ids = _first_bool(additional_info.get("tts_pass_token_ids"), False)
tts_info = {
"task_type": [task_type],
"language": [language],
"instruct": [instruct],
"max_new_tokens": [int(_first_value(additional_info.get("tts_max_new_tokens"), 2048))],
}
if pass_token_ids and assistant_token_ids_for_len:
tts_info[PRECOMPUTED_TEXT_IDS_KEY] = [assistant_token_ids_for_len]
else:
tts_info["text"] = [text]
if ref_code_len is not None:
tts_info["ref_code_length"] = [int(ref_code_len)]
prompt_len = _estimate_tts_prompt_len_from_token_ids(
assistant_token_ids_for_len if assistant_token_ids_for_len else [0] * max(0, len(text)),
task_type=str(task_type),
language=str(language),
instruct=str(instruct),
x_vector_only_mode=x_vector_only_mode,
non_streaming_mode=non_streaming_mode,
ref_code_len=ref_code_len,
)
if task_type == "Base":
ref_audio = ref_audio or _first_value(additional_info.get("tts_ref_audio"), None)
ref_text = _first_value(additional_info.get("tts_ref_text"), None)
if not ref_audio or not ref_text:
raise ValueError("AURA Base TTS requires tts_ref_audio and tts_ref_text.")
x_vector_only_mode = _first_bool(additional_info.get("tts_x_vector_only_mode"), False)
tts_info["ref_audio"] = [ref_audio]
tts_info["ref_text"] = [ref_text]
tts_info["x_vector_only_mode"] = [x_vector_only_mode]
elif task_type == "CustomVoice":
tts_info["speaker"] = [
_normalize_qwen3_tts_speaker(_first_value(additional_info.get("tts_speaker"), "Vivian"))
]
next_inputs.append(
OmniTokensPrompt(
prompt_token_ids=[0] * prompt_len,
additional_information=tts_info,
multi_modal_data=None,
mm_processor_kwargs=None,
)
)
return next_inputs