[Feature] : Support disaggregated inference pipeline for Qwen3_TTS (#1161)
Signed-off-by: Sy03 <1370724210@qq.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
5295f0f81b
commit
efbe4116ff
23 files changed
+2970
-2592
No files matched your search
@@ -176,7 +176,7 @@ steps:
|
||||
# type: DirectoryOrCreate
|
||||
|
||||
- label: "Omni Model Test"
|
||||
timeout_in_minutes: 15
|
||||
timeout_in_minutes: 20
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
|
||||
@@ -4,18 +4,21 @@ Provides single and batch sample inputs for CustomVoice, VoiceDesign, and Base
|
||||
tasks, then runs Omni generation and saves output wav files.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import soundfile as sf
|
||||
import torch
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
from vllm import SamplingParams
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
from vllm_omni import Omni
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QueryResult(NamedTuple):
|
||||
"""Container for a prepared Omni request."""
|
||||
@@ -24,6 +27,44 @@ class QueryResult(NamedTuple):
|
||||
model_name: str
|
||||
|
||||
|
||||
def _estimate_prompt_len(
|
||||
additional_information: dict[str, Any],
|
||||
model_name: str,
|
||||
_cache: dict[str, Any] = {},
|
||||
) -> int:
|
||||
"""Estimate prompt_token_ids placeholder length for the Talker stage.
|
||||
|
||||
The AR Talker replaces all input embeddings via ``preprocess``, so the
|
||||
placeholder values are irrelevant but the **length** must match the
|
||||
embeddings that ``preprocess`` will produce.
|
||||
"""
|
||||
try:
|
||||
from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import Qwen3TTSConfig
|
||||
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import (
|
||||
Qwen3TTSTalkerForConditionalGeneration,
|
||||
)
|
||||
|
||||
if model_name not in _cache:
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True, padding_side="left")
|
||||
cfg = Qwen3TTSConfig.from_pretrained(model_name, trust_remote_code=True)
|
||||
_cache[model_name] = (tok, getattr(cfg, "talker_config", None))
|
||||
|
||||
tok, tcfg = _cache[model_name]
|
||||
task_type = (additional_information.get("task_type") or ["CustomVoice"])[0]
|
||||
return Qwen3TTSTalkerForConditionalGeneration.estimate_prompt_len_from_additional_information(
|
||||
additional_information=additional_information,
|
||||
task_type=task_type,
|
||||
tokenize_prompt=lambda t: tok(t, padding=False)["input_ids"],
|
||||
codec_language_id=getattr(tcfg, "codec_language_id", None),
|
||||
spk_is_dialect=getattr(tcfg, "spk_is_dialect", None),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to estimate prompt length, using fallback 2048: %s", exc)
|
||||
return 2048
|
||||
|
||||
|
||||
def get_custom_voice_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
"""Build CustomVoice sample inputs.
|
||||
|
||||
@@ -34,6 +75,7 @@ def get_custom_voice_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
QueryResult with Omni inputs and the CustomVoice model path.
|
||||
"""
|
||||
task_type = "CustomVoice"
|
||||
model_name = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
||||
if use_batch_sample:
|
||||
texts = ["其实我真的有发现,我是一个特别善于观察别人情绪的人。", "She said she would be here by noon."]
|
||||
instructs = ["", "Very happy."]
|
||||
@@ -41,18 +83,18 @@ def get_custom_voice_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
speakers = ["Vivian", "Ryan"]
|
||||
inputs = []
|
||||
for text, instruct, language, speaker in zip(texts, instructs, languages, speakers):
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
additional_information = {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"instruct": [instruct],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"max_new_tokens": [2048],
|
||||
}
|
||||
inputs.append(
|
||||
{
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"instruct": [instruct],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -60,21 +102,21 @@ def get_custom_voice_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
language = "Chinese"
|
||||
speaker = "Vivian"
|
||||
instruct = "用特别愤怒的语气说"
|
||||
prompts = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
additional_information = {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
}
|
||||
inputs = {
|
||||
"prompt": prompts,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
return QueryResult(
|
||||
inputs=inputs,
|
||||
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
|
||||
model_name=model_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -88,6 +130,7 @@ def get_voice_design_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
QueryResult with Omni inputs and the VoiceDesign model path.
|
||||
"""
|
||||
task_type = "VoiceDesign"
|
||||
model_name = "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign"
|
||||
if use_batch_sample:
|
||||
texts = [
|
||||
"哥哥,你回来啦,人家等了你好久好久了,要抱抱!",
|
||||
@@ -100,39 +143,39 @@ def get_voice_design_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
languages = ["Chinese", "English"]
|
||||
inputs = []
|
||||
for text, instruct, language in zip(texts, instructs, languages):
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs.append(
|
||||
{
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
"non_streaming_mode": [True],
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
text = "哥哥,你回来啦,人家等了你好久好久了,要抱抱!"
|
||||
instruct = "体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。"
|
||||
language = "Chinese"
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs = {
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
additional_information = {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
"non_streaming_mode": [True],
|
||||
},
|
||||
}
|
||||
inputs.append(
|
||||
{
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
)
|
||||
else:
|
||||
text = "哥哥,你回来啦,人家等了你好久好久了,要抱抱!"
|
||||
instruct = "体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。"
|
||||
language = "Chinese"
|
||||
additional_information = {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
"non_streaming_mode": [True],
|
||||
}
|
||||
inputs = {
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
return QueryResult(
|
||||
inputs=inputs,
|
||||
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
|
||||
model_name=model_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -147,6 +190,7 @@ def get_base_query(use_batch_sample: bool = False, mode_tag: str = "icl") -> Que
|
||||
QueryResult with Omni inputs and the Base model path.
|
||||
"""
|
||||
task_type = "Base"
|
||||
model_name = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||
ref_audio_path_1 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
|
||||
ref_audio_single = ref_audio_path_1
|
||||
ref_text_single = (
|
||||
@@ -163,38 +207,38 @@ def get_base_query(use_batch_sample: bool = False, mode_tag: str = "icl") -> Que
|
||||
syn_lang_batch = ["Chinese", "English"]
|
||||
inputs = []
|
||||
for text, language in zip(syn_text_batch, syn_lang_batch):
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs.append(
|
||||
{
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"ref_audio": [ref_audio_single],
|
||||
"ref_text": [ref_text_single],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"x_vector_only_mode": [x_vector_only_mode],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
prompt = f"<|im_start|>assistant\n{syn_text_single}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs = {
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
additional_information = {
|
||||
"task_type": [task_type],
|
||||
"ref_audio": [ref_audio_single],
|
||||
"ref_text": [ref_text_single],
|
||||
"text": [syn_text_single],
|
||||
"language": [syn_lang_single],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"x_vector_only_mode": [x_vector_only_mode],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
}
|
||||
inputs.append(
|
||||
{
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
)
|
||||
else:
|
||||
additional_information = {
|
||||
"task_type": [task_type],
|
||||
"ref_audio": [ref_audio_single],
|
||||
"ref_text": [ref_text_single],
|
||||
"text": [syn_text_single],
|
||||
"language": [syn_lang_single],
|
||||
"x_vector_only_mode": [x_vector_only_mode],
|
||||
"max_new_tokens": [2048],
|
||||
}
|
||||
inputs = {
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
return QueryResult(
|
||||
inputs=inputs,
|
||||
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-Base",
|
||||
model_name=model_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -223,30 +267,22 @@ def main(args):
|
||||
stage_init_timeout=args.stage_init_timeout,
|
||||
)
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0.9,
|
||||
top_p=1.0,
|
||||
top_k=50,
|
||||
max_tokens=2048,
|
||||
seed=42,
|
||||
detokenize=False,
|
||||
repetition_penalty=1.05,
|
||||
)
|
||||
|
||||
sampling_params_list = [
|
||||
sampling_params,
|
||||
]
|
||||
|
||||
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
omni_generator = omni.generate(query_result.inputs, sampling_params_list)
|
||||
omni_generator = omni.generate(query_result.inputs, sampling_params_list=None)
|
||||
for stage_outputs in omni_generator:
|
||||
for output in stage_outputs.request_output:
|
||||
request_id = output.request_id
|
||||
audio_tensor = output.outputs[0].multimodal_output["audio"]
|
||||
audio_data = output.outputs[0].multimodal_output["audio"]
|
||||
# async_chunk mode returns a list of chunks; concatenate them.
|
||||
if isinstance(audio_data, list):
|
||||
audio_tensor = torch.cat(audio_data, dim=-1)
|
||||
else:
|
||||
audio_tensor = audio_data
|
||||
output_wav = os.path.join(output_dir, f"output_{request_id}.wav")
|
||||
audio_samplerate = output.outputs[0].multimodal_output["sr"].item()
|
||||
sr_val = output.outputs[0].multimodal_output["sr"]
|
||||
audio_samplerate = sr_val.item() if hasattr(sr_val, "item") else int(sr_val[-1])
|
||||
# Convert to numpy array and ensure correct format
|
||||
audio_numpy = audio_tensor.float().detach().cpu().numpy()
|
||||
|
||||
|
||||
@@ -310,10 +310,12 @@ class TestTTSMethods:
|
||||
speech_server.engine_client.stage_list = [mock_stage]
|
||||
assert speech_server._is_tts_model() is True
|
||||
|
||||
def test_build_tts_prompt(self, speech_server):
|
||||
"""Test TTS prompt format."""
|
||||
prompt = speech_server._build_tts_prompt("Hello")
|
||||
assert prompt == "<|im_start|>assistant\nHello<|im_end|>\n<|im_start|>assistant\n"
|
||||
def test_estimate_prompt_len_fallback(self, speech_server):
|
||||
"""Test prompt length estimation falls back to 2048 when model is unavailable."""
|
||||
tts_params = {"text": ["Hello"], "task_type": ["CustomVoice"]}
|
||||
result = speech_server._estimate_prompt_len(tts_params)
|
||||
# Without a real model, it should fall back to 2048.
|
||||
assert result == 2048
|
||||
|
||||
def test_validate_tts_request_basic(self, speech_server):
|
||||
"""Test basic validation cases."""
|
||||
|
||||
@@ -69,6 +69,7 @@ def _make_runner(req_ids=("r1", "r2"), hidden_size=4):
|
||||
runner.text_step = DummyBuffer(torch.zeros((bsz, hidden_size), dtype=torch.float32))
|
||||
|
||||
runner.talker_mtp = DummyTalkerMTP()
|
||||
runner.model = SimpleNamespace(talker_mtp_output_key="code_predictor_codes")
|
||||
runner.vllm_config = object()
|
||||
|
||||
# Provide a minimal implementation that returns the expected 4-tuple.
|
||||
|
||||
@@ -58,6 +58,7 @@ class OmniModelConfig(ModelConfig):
|
||||
}
|
||||
)
|
||||
omni_kv_config: dict | None = None
|
||||
codec_frame_rate_hz: float | None = None
|
||||
|
||||
@property
|
||||
def registry(self):
|
||||
@@ -128,6 +129,21 @@ class OmniModelConfig(ModelConfig):
|
||||
video_pruning_rate=video_pruning_rate,
|
||||
)
|
||||
|
||||
# Qwen3-TTS: infer codec frame rate from the model config for online serving.
|
||||
if self.codec_frame_rate_hz is None and self.model_arch == "Qwen3TTSTalkerForConditionalGenerationARVLLM":
|
||||
talker_cfg = getattr(self.hf_config, "talker_config", None)
|
||||
if isinstance(talker_cfg, dict):
|
||||
pos_per_sec = talker_cfg.get("position_id_per_seconds")
|
||||
else:
|
||||
pos_per_sec = getattr(talker_cfg, "position_id_per_seconds", None)
|
||||
if pos_per_sec is not None:
|
||||
try:
|
||||
fps = float(pos_per_sec)
|
||||
except Exception:
|
||||
fps = None
|
||||
if fps is not None and fps > 0:
|
||||
self.codec_frame_rate_hz = fps
|
||||
|
||||
# Override hf_text_config with omni-specific logic for multi-stage models
|
||||
# (e.g., thinker_config, talker_config)
|
||||
new_hf_text_config = self.draw_hf_text_config()
|
||||
|
||||
@@ -62,7 +62,6 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
while req_index < len(self.running) and token_budget > 0:
|
||||
request = self.running[req_index]
|
||||
# OMNI: Skip requests that are not in self.requests
|
||||
# This can happen when connector marks request as finished and it's removed from requests
|
||||
if request.request_id not in self.requests or (
|
||||
self.chunk_transfer_adapter is None and request.status == RequestStatus.FINISHED_STOPPED
|
||||
):
|
||||
@@ -71,7 +70,25 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
continue
|
||||
|
||||
num_computed_tokens = request.num_computed_tokens
|
||||
required_tokens = max(len(request.prompt_token_ids) - num_computed_tokens, 1)
|
||||
required_tokens = len(request.prompt_token_ids) - num_computed_tokens
|
||||
# async_chunk: don't schedule placeholder tokens when no new chunk is available.
|
||||
if required_tokens <= 0:
|
||||
if (
|
||||
self.chunk_transfer_adapter is not None
|
||||
and request.request_id in self.chunk_transfer_adapter.finished_requests
|
||||
):
|
||||
request.status = RequestStatus.FINISHED_STOPPED
|
||||
# Upstream may finish with no terminal tokens; append one pad token so we can emit FINISHED.
|
||||
if len(request.prompt_token_ids) <= num_computed_tokens:
|
||||
request.prompt_token_ids.append(0)
|
||||
try:
|
||||
request._all_token_ids.append(0) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
required_tokens = len(request.prompt_token_ids) - num_computed_tokens
|
||||
else:
|
||||
req_index += 1
|
||||
continue
|
||||
num_new_tokens = min(required_tokens, token_budget)
|
||||
new_blocks = self.kv_cache_manager.allocate_slots(
|
||||
request,
|
||||
@@ -109,6 +126,20 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
self.waiting.pop_request()
|
||||
continue
|
||||
|
||||
# async_chunk: wait for the first upstream chunk (don't start with placeholders).
|
||||
if self.chunk_transfer_adapter is not None and len(request.prompt_token_ids) == 0:
|
||||
if request.request_id in self.chunk_transfer_adapter.finished_requests:
|
||||
request.status = RequestStatus.FINISHED_STOPPED
|
||||
request.prompt_token_ids.append(0)
|
||||
try:
|
||||
request._all_token_ids.append(0) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.waiting.pop_request()
|
||||
skipped_waiting_requests.prepend_request(request)
|
||||
continue
|
||||
|
||||
# Uniformly treat as diffusion. A feature flag can be added later
|
||||
# via config or request tag.
|
||||
|
||||
@@ -145,11 +176,13 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
|
||||
# If fast path scheduled none, fall back to the original scheduling
|
||||
if not num_scheduled_tokens:
|
||||
res = super().schedule()
|
||||
if self.chunk_transfer_adapter:
|
||||
# Don't fall back: base scheduler doesn't handle async_chunk
|
||||
# requests with empty prompt_token_ids.
|
||||
self.chunk_transfer_adapter.restore_queues(self.waiting, self.running)
|
||||
self.chunk_transfer_adapter.postprocess_scheduler_output(res)
|
||||
return res
|
||||
else:
|
||||
res = super().schedule()
|
||||
return res
|
||||
|
||||
# Compute common prefix blocks (aligned with v1)
|
||||
num_common_prefix_blocks = [0] * len(self.kv_cache_config.kv_cache_groups)
|
||||
@@ -318,9 +351,8 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
continue
|
||||
request = self.requests.get(req_id)
|
||||
if request is None or request.is_finished():
|
||||
# The request is already finished. This can happen if the
|
||||
# request is aborted while the model is executing it (e.g.,
|
||||
# in pipeline parallelism or async scheduling).
|
||||
# Request may already be finished (e.g., aborted during
|
||||
# execution / pipeline parallelism / async scheduling).
|
||||
continue
|
||||
|
||||
req_index = model_runner_output.req_id_to_index[req_id]
|
||||
@@ -360,8 +392,14 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
routed_experts = None
|
||||
|
||||
# Diffusion request: completes in one step; mark finished and free resources
|
||||
if request.status == RequestStatus.FINISHED_STOPPED or (
|
||||
self.chunk_transfer_adapter is None and request.num_computed_tokens >= request.num_prompt_tokens
|
||||
if (
|
||||
request.status == RequestStatus.FINISHED_STOPPED
|
||||
or (self.chunk_transfer_adapter is None and request.num_computed_tokens >= request.num_prompt_tokens)
|
||||
or (
|
||||
self.chunk_transfer_adapter is not None
|
||||
and request.request_id in self.chunk_transfer_adapter.finished_requests
|
||||
and request.num_computed_tokens >= len(request.prompt_token_ids)
|
||||
)
|
||||
):
|
||||
request.status = RequestStatus.FINISHED_STOPPED
|
||||
# Optional: set a stop_reason for front-end clarity
|
||||
@@ -375,15 +413,11 @@ class OmniGenerationScheduler(VLLMScheduler):
|
||||
finished = self._handle_stopped_request(request)
|
||||
if finished:
|
||||
kv_transfer_params = self._free_request(request)
|
||||
if status_before_stop == RequestStatus.RUNNING:
|
||||
stopped_running_reqs.add(request)
|
||||
elif status_before_stop == RequestStatus.WAITING_FOR_CHUNK:
|
||||
# In async chunk mode, request may be in either queue.
|
||||
# Remove from both to avoid stale queue entries.
|
||||
if status_before_stop == RequestStatus.WAITING_FOR_CHUNK:
|
||||
stopped_running_reqs.add(request)
|
||||
stopped_preempted_reqs.add(request)
|
||||
else:
|
||||
stopped_preempted_reqs.add(request)
|
||||
stopped_running_reqs.add(request)
|
||||
|
||||
# Extract sample logprobs if needed.
|
||||
if request.sampling_params is not None and request.sampling_params.logprobs is not None and logprobs:
|
||||
|
||||
@@ -179,11 +179,17 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
else:
|
||||
if payload_data.get("finished"):
|
||||
self.finished_requests.add(req_id)
|
||||
req.status = RequestStatus.FINISHED_STOPPED
|
||||
|
||||
req.prompt_token_ids = payload_data.get("code_predictor_codes", [])
|
||||
# req.prompt_token_ids = payload_data.get("code_predictor_codes", [])
|
||||
# req.num_computed_tokens = 0
|
||||
new_ids = payload_data.get("code_predictor_codes", [])
|
||||
req.prompt_token_ids = new_ids
|
||||
req.num_computed_tokens = 0
|
||||
|
||||
# Empty chunk with more data expected: keep polling.
|
||||
if not new_ids and not payload_data.get("finished"):
|
||||
return
|
||||
|
||||
# Mark as finished for consumption
|
||||
with self.lock:
|
||||
self._finished_load_reqs.add(req_id)
|
||||
@@ -308,7 +314,6 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
# of schedule, but have not scheduled
|
||||
continue
|
||||
if request.request_id in self.finished_requests:
|
||||
request.additional_information = {}
|
||||
continue
|
||||
# Requests that waiting for chunk
|
||||
self.load_async(request)
|
||||
|
||||
@@ -390,7 +390,8 @@ class AsyncOmni(OmniBase):
|
||||
submit_flag = False
|
||||
prompt_token_ids = engine_outputs.prompt_token_ids
|
||||
engine_input = copy.deepcopy(prompt)
|
||||
engine_input["prompt_token_ids"] = [0] * compute_talker_prompt_ids_length(prompt_token_ids)
|
||||
next_prompt_len = max(1, compute_talker_prompt_ids_length(prompt_token_ids))
|
||||
engine_input["prompt_token_ids"] = [0] * next_prompt_len
|
||||
engine_input["multi_modal_data"] = engine_input["mm_processor_kwargs"] = None
|
||||
for i in range(1, len(self.stage_list)):
|
||||
task = {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import io
|
||||
import ipaddress
|
||||
import socket
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from fastapi import Request
|
||||
from fastapi.responses import Response
|
||||
from vllm.entrypoints.openai.engine.serving import OpenAIServing
|
||||
@@ -17,6 +25,19 @@ from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_REF_AUDIO_TIMEOUT_S = 15
|
||||
_REF_AUDIO_MAX_BYTES = 50 * 1024 * 1024 # 50 MB
|
||||
_REF_AUDIO_BLOCKED_NETWORKS = [
|
||||
ipaddress.ip_network("127.0.0.0/8"),
|
||||
ipaddress.ip_network("10.0.0.0/8"),
|
||||
ipaddress.ip_network("172.16.0.0/12"),
|
||||
ipaddress.ip_network("192.168.0.0/16"),
|
||||
ipaddress.ip_network("169.254.0.0/16"),
|
||||
ipaddress.ip_network("::1/128"),
|
||||
ipaddress.ip_network("fc00::/7"),
|
||||
ipaddress.ip_network("fe80::/10"),
|
||||
]
|
||||
|
||||
# TTS Configuration (currently supports Qwen3-TTS)
|
||||
_TTS_MODEL_STAGES: set[str] = {"qwen3_tts"}
|
||||
_TTS_LANGUAGES: set[str] = {
|
||||
@@ -43,6 +64,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
# Load supported speakers
|
||||
self.supported_speakers = self._load_supported_speakers()
|
||||
logger.info(f"Loaded {len(self.supported_speakers)} supported speakers: {sorted(self.supported_speakers)}")
|
||||
self._tts_tokenizer = None
|
||||
|
||||
def _load_supported_speakers(self) -> set[str]:
|
||||
"""Load supported speakers (case-insensitive) from the model configuration."""
|
||||
@@ -62,6 +84,36 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
return set()
|
||||
|
||||
def _estimate_prompt_len(self, tts_params: dict[str, Any]) -> int:
|
||||
"""Estimate prompt length so the placeholder matches model-side embeddings."""
|
||||
try:
|
||||
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import (
|
||||
Qwen3TTSTalkerForConditionalGeneration,
|
||||
)
|
||||
|
||||
if self._tts_tokenizer is None:
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
model_name = self.engine_client.model_config.model
|
||||
self._tts_tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_name,
|
||||
trust_remote_code=True,
|
||||
padding_side="left",
|
||||
)
|
||||
hf_config = self.engine_client.model_config.hf_config
|
||||
talker_config = hf_config.talker_config
|
||||
task_type = (tts_params.get("task_type") or ["CustomVoice"])[0]
|
||||
return Qwen3TTSTalkerForConditionalGeneration.estimate_prompt_len_from_additional_information(
|
||||
additional_information=tts_params,
|
||||
task_type=task_type,
|
||||
tokenize_prompt=lambda t: self._tts_tokenizer(t, padding=False)["input_ids"],
|
||||
codec_language_id=getattr(talker_config, "codec_language_id", None),
|
||||
spk_is_dialect=getattr(talker_config, "spk_is_dialect", None),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to estimate TTS prompt length, using fallback 2048: %s", e)
|
||||
return 2048
|
||||
|
||||
def _is_tts_model(self) -> bool:
|
||||
"""Check if the current model is a supported TTS model."""
|
||||
stage_list = getattr(self.engine_client, "stage_list", None)
|
||||
@@ -125,9 +177,44 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
return None
|
||||
|
||||
def _build_tts_prompt(self, text: str) -> str:
|
||||
"""Build TTS prompt from input text."""
|
||||
return f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
@staticmethod
|
||||
async def _resolve_ref_audio(ref_audio_str: str) -> tuple[list[float], int]:
|
||||
"""Resolve ref_audio URL/base64 to (wav_samples, sample_rate)."""
|
||||
parsed = urlparse(ref_audio_str)
|
||||
|
||||
def _check_ssrf(url: str) -> None:
|
||||
host = urlparse(url).hostname
|
||||
if not host:
|
||||
raise ValueError("ref_audio URL must include a hostname")
|
||||
for info in socket.getaddrinfo(host, None):
|
||||
ip_str = str(info[4][0]).split("%", 1)[0]
|
||||
addr = ipaddress.ip_address(ip_str)
|
||||
if any(addr in net for net in _REF_AUDIO_BLOCKED_NETWORKS):
|
||||
raise ValueError(f"ref_audio URL resolves to blocked address: {addr}")
|
||||
|
||||
def _fetch_sync() -> tuple[np.ndarray, int]:
|
||||
if parsed.scheme in ("http", "https"):
|
||||
_check_ssrf(ref_audio_str)
|
||||
with urlopen(ref_audio_str, timeout=_REF_AUDIO_TIMEOUT_S) as resp:
|
||||
data = resp.read(_REF_AUDIO_MAX_BYTES + 1)
|
||||
if len(data) > _REF_AUDIO_MAX_BYTES:
|
||||
raise ValueError(f"ref_audio URL exceeds {_REF_AUDIO_MAX_BYTES} bytes")
|
||||
buf = io.BytesIO(data)
|
||||
elif ref_audio_str.startswith("data:"):
|
||||
b64 = ref_audio_str
|
||||
if "," in b64:
|
||||
b64 = b64.split(",", 1)[1]
|
||||
buf = io.BytesIO(base64.b64decode(b64))
|
||||
else:
|
||||
raise ValueError("ref_audio must be an http(s) URL or data: base64 URI")
|
||||
audio, sr = sf.read(buf, dtype="float32", always_2d=False)
|
||||
if isinstance(audio, np.ndarray) and audio.ndim > 1:
|
||||
audio = np.mean(audio, axis=-1)
|
||||
return np.asarray(audio, dtype=np.float32), int(sr)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
wav_np, sr = await loop.run_in_executor(None, _fetch_sync)
|
||||
return wav_np.tolist(), sr
|
||||
|
||||
def _build_tts_params(self, request: OpenAICreateSpeechRequest) -> dict[str, Any]:
|
||||
"""Build TTS parameters from request.
|
||||
@@ -164,9 +251,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
else:
|
||||
params["instruct"] = [""]
|
||||
|
||||
# Voice clone parameters (used with Base task)
|
||||
if request.ref_audio is not None:
|
||||
params["ref_audio"] = [request.ref_audio]
|
||||
# Voice clone: ref_audio resolved in create_speech(), not here.
|
||||
if request.ref_text is not None:
|
||||
params["ref_text"] = [request.ref_text]
|
||||
if request.x_vector_only_mode is not None:
|
||||
@@ -221,11 +306,19 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
if validation_error:
|
||||
return self.create_error_response(validation_error)
|
||||
|
||||
# Build TTS parameters and prompt
|
||||
# Must use prompt_token_ids (not text prompt): the AR Talker
|
||||
# operates on codec tokens; text token IDs exceed codec vocab.
|
||||
# model.preprocess replaces all embeddings, so placeholder value
|
||||
# is irrelevant -- but length must match to avoid excess padding.
|
||||
tts_params = self._build_tts_params(request)
|
||||
prompt_text = self._build_tts_prompt(request.input)
|
||||
|
||||
if request.ref_audio is not None:
|
||||
wav_list, sr = await self._resolve_ref_audio(request.ref_audio)
|
||||
tts_params["ref_audio"] = [[wav_list, sr]]
|
||||
|
||||
ph_len = self._estimate_prompt_len(tts_params)
|
||||
prompt = {
|
||||
"prompt": prompt_text,
|
||||
"prompt_token_ids": [1] * ph_len,
|
||||
"additional_information": tts_params,
|
||||
}
|
||||
else:
|
||||
@@ -282,6 +375,11 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
if hasattr(sample_rate, "item"):
|
||||
sample_rate = sample_rate.item()
|
||||
|
||||
# Streaming accumulates chunks as a list; concat first.
|
||||
if isinstance(audio_tensor, list):
|
||||
import torch
|
||||
|
||||
audio_tensor = torch.cat(audio_tensor, dim=-1)
|
||||
# Convert tensor to numpy
|
||||
if hasattr(audio_tensor, "float"):
|
||||
audio_tensor = audio_tensor.float().detach().cpu().numpy()
|
||||
|
||||
@@ -504,19 +504,39 @@ class Qwen3TTSConfig(PretrainedConfig):
|
||||
self.tts_bos_token_id = tts_bos_token_id
|
||||
self.tts_eos_token_id = tts_eos_token_id
|
||||
|
||||
# TODO: remove these dummy values after
|
||||
self.image_token_id = 0 # dummy image token id
|
||||
self.video_token_id = 0 # dummy video token id
|
||||
self.vision_start_token_id = 0 # dummy vision start token id
|
||||
# Dummy vision token IDs that must never collide with real codec tokens.
|
||||
# mrope scans prompt_token_ids for these; using -1 ensures no false match.
|
||||
self.image_token_id = -1
|
||||
self.video_token_id = -1
|
||||
self.vision_start_token_id = -1
|
||||
self.vision_config = PretrainedConfig() # dummy vision config
|
||||
self.vision_config.spatial_merge_size = 1
|
||||
|
||||
@property
|
||||
def codec_frame_rate_hz(self) -> float | None:
|
||||
pos_per_sec = getattr(self.talker_config, "position_id_per_seconds", None)
|
||||
if pos_per_sec is None:
|
||||
return None
|
||||
try:
|
||||
fps = float(pos_per_sec)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return fps if fps > 0 else None
|
||||
|
||||
def get_text_config(self, **kwargs):
|
||||
# vLLM expects text config to expose hidden_size/num_attention_heads.
|
||||
# For Qwen3 TTS, the talker config is the text model config.
|
||||
config = self.talker_config
|
||||
# if hasattr(config, "rope_parameters"):
|
||||
# delattr(config, "rope_parameters")
|
||||
# Code2Wav is a pure convolutional waveform decoder; it does NOT use
|
||||
# rotary position embeddings. When hf_overrides sets architectures
|
||||
# to [Qwen3TTSCode2Wav], strip rope_parameters so that the model
|
||||
# runner sees uses_mrope == False and skips mrope position computation
|
||||
# on codec tokens. Each stage loads its own config instance, so this
|
||||
# in-place mutation does not affect the Talker stage.
|
||||
archs = getattr(self, "architectures", []) or []
|
||||
if any("Code2Wav" in str(a) for a in archs):
|
||||
if hasattr(config, "rope_parameters"):
|
||||
delattr(config, "rope_parameters")
|
||||
return config
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,102 +0,0 @@
|
||||
# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from transformers.feature_extraction_utils import BatchFeature
|
||||
from transformers.processing_utils import ProcessingKwargs, ProcessorMixin
|
||||
|
||||
|
||||
class Qwen3TTSProcessorKwargs(ProcessingKwargs, total=False):
|
||||
_defaults = {
|
||||
"text_kwargs": {
|
||||
"padding": False,
|
||||
"padding_side": "left",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Qwen3TTSProcessor(ProcessorMixin):
|
||||
r"""
|
||||
Constructs a Qwen3TTS processor.
|
||||
|
||||
Args:
|
||||
tokenizer ([`Qwen2TokenizerFast`], *optional*):
|
||||
The text tokenizer.
|
||||
chat_template (`Optional[str]`, *optional*):
|
||||
The Jinja template to use for formatting the conversation.
|
||||
If not provided, the default chat template is used.
|
||||
"""
|
||||
|
||||
attributes = ["tokenizer"]
|
||||
tokenizer_class = ("Qwen2Tokenizer", "Qwen2TokenizerFast")
|
||||
|
||||
def __init__(self, tokenizer=None, chat_template=None):
|
||||
super().__init__(tokenizer, chat_template=chat_template)
|
||||
|
||||
def __call__(self, text=None, **kwargs) -> BatchFeature:
|
||||
"""
|
||||
Main method to prepare for the model one or several sequences(s) and audio(s).
|
||||
This method forwards the `text` and `kwargs` arguments to
|
||||
Qwen2TokenizerFast's [`~Qwen2TokenizerFast.__call__`] if `text` is not `None`
|
||||
to encode the text.
|
||||
|
||||
Args:
|
||||
text (`str`, `List[str]`, `List[List[str]]`):
|
||||
The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
|
||||
(pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
|
||||
`is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
|
||||
"""
|
||||
|
||||
if text is None:
|
||||
raise ValueError("You need to specify either a `text` input to process.")
|
||||
|
||||
output_kwargs = self._merge_kwargs(
|
||||
Qwen3TTSProcessorKwargs,
|
||||
tokenizer_init_kwargs=self.tokenizer.init_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
if not isinstance(text, list):
|
||||
text = [text]
|
||||
|
||||
texts_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
|
||||
|
||||
return BatchFeature(
|
||||
data={**texts_inputs},
|
||||
tensor_type=kwargs.get("return_tensors"),
|
||||
)
|
||||
|
||||
def batch_decode(self, *args, **kwargs):
|
||||
"""
|
||||
This method forwards all its arguments to Qwen2TokenizerFast's [`~PreTrainedTokenizer.batch_decode`]. Please
|
||||
refer to the docstring of this method for more information.
|
||||
"""
|
||||
return self.tokenizer.batch_decode(*args, **kwargs)
|
||||
|
||||
def decode(self, *args, **kwargs):
|
||||
"""
|
||||
This method forwards all its arguments to Qwen2TokenizerFast's [`~PreTrainedTokenizer.decode`]. Please refer to
|
||||
the docstring of this method for more information.
|
||||
"""
|
||||
return self.tokenizer.decode(*args, **kwargs)
|
||||
|
||||
def apply_chat_template(self, conversations, chat_template=None, **kwargs):
|
||||
if isinstance(conversations[0], dict):
|
||||
conversations = [conversations]
|
||||
return super().apply_chat_template(conversations, chat_template, **kwargs)
|
||||
|
||||
@property
|
||||
def model_input_names(self):
|
||||
tokenizer_input_names = self.tokenizer.model_input_names
|
||||
return list(dict.fromkeys(tokenizer_input_names))
|
||||
|
||||
|
||||
__all__ = ["Qwen3TTSProcessor"]
|
||||
@@ -0,0 +1,239 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from transformers.utils.hub import cached_file
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
from .qwen3_tts_tokenizer import Qwen3TTSTokenizer
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class Qwen3TTSCode2Wav(nn.Module):
|
||||
"""Stage-1 code2wav model for Qwen3-TTS (GenerationModelRunner).
|
||||
Consumes frame-aligned codec tokens from input_ids and decodes waveform via SpeechTokenizer."""
|
||||
|
||||
input_modalities = "audio"
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.vllm_config = vllm_config
|
||||
self.model_path = vllm_config.model_config.model
|
||||
|
||||
self.have_multimodal_outputs = True
|
||||
self.has_preprocess = False
|
||||
self.has_postprocess = False
|
||||
# Generation-only stage (no logits / sampling).
|
||||
self.requires_raw_input_tokens = True
|
||||
|
||||
self._speech_tokenizer: Qwen3TTSTokenizer | None = None
|
||||
self._num_quantizers: int | None = None
|
||||
self._decode_upsample_rate: int | None = None
|
||||
self._output_sample_rate: int | None = None
|
||||
self._logged_codec_stats = False
|
||||
|
||||
@staticmethod
|
||||
def _module_device(module: nn.Module) -> torch.device:
|
||||
try:
|
||||
return next(module.parameters()).device
|
||||
except StopIteration:
|
||||
for _, buf in module.named_buffers(recurse=True):
|
||||
return buf.device
|
||||
return torch.device("cpu")
|
||||
|
||||
def _ensure_speech_tokenizer_loaded(self) -> Qwen3TTSTokenizer:
|
||||
if self._speech_tokenizer is not None:
|
||||
return self._speech_tokenizer
|
||||
|
||||
# Locate speech_tokenizer dir from HF cache (or local path).
|
||||
cfg_path = cached_file(self.model_path, "speech_tokenizer/config.json")
|
||||
if cfg_path is None:
|
||||
raise ValueError(f"{self.model_path}/speech_tokenizer/config.json not found")
|
||||
speech_tokenizer_dir = os.path.dirname(cfg_path)
|
||||
|
||||
# Stage-1 only needs decode; skip HF feature extractor to avoid heavy optional deps.
|
||||
# Still require preprocessor_config.json (use cached_file so online runs can fetch it).
|
||||
prep_cfg = cached_file(self.model_path, "speech_tokenizer/preprocessor_config.json")
|
||||
if prep_cfg is None:
|
||||
raise ValueError(
|
||||
f"{self.model_path}/speech_tokenizer/preprocessor_config.json not found. "
|
||||
"Please make sure the checkpoint contains the required HF preprocessing files."
|
||||
)
|
||||
|
||||
tok = Qwen3TTSTokenizer.from_pretrained(
|
||||
speech_tokenizer_dir,
|
||||
torch_dtype=torch.bfloat16,
|
||||
load_feature_extractor=False,
|
||||
)
|
||||
|
||||
# Align device with vLLM worker, then read back from module.
|
||||
if tok.model is not None:
|
||||
tok.model.to(device=self.vllm_config.device_config.device)
|
||||
tok.device = self._module_device(tok.model)
|
||||
|
||||
# Derive codec group count and rates from tokenizer config.
|
||||
dec_cfg = getattr(tok.model.config, "decoder_config", None)
|
||||
num_q = getattr(dec_cfg, "num_quantizers", None) if dec_cfg is not None else None
|
||||
if num_q is None:
|
||||
raise ValueError("speech_tokenizer decoder_config.num_quantizers not found")
|
||||
num_q = int(num_q)
|
||||
if num_q <= 0:
|
||||
raise ValueError(f"Invalid speech_tokenizer num_quantizers={num_q}")
|
||||
|
||||
try:
|
||||
upsample = int(tok.get_decode_upsample_rate())
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to get decode upsample rate: {e}") from e
|
||||
if upsample <= 0:
|
||||
raise ValueError(f"Invalid decode upsample rate: {upsample}")
|
||||
|
||||
try:
|
||||
out_sr = int(tok.get_output_sample_rate())
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to get output sample rate: {e}") from e
|
||||
|
||||
self._speech_tokenizer = tok
|
||||
self._num_quantizers = num_q
|
||||
self._decode_upsample_rate = upsample
|
||||
self._output_sample_rate = out_sr
|
||||
return tok
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
|
||||
# This stage ignores token embeddings. Keep a stable dummy embedding for vLLM runner.
|
||||
if input_ids.numel() == 0:
|
||||
return torch.empty((0, 1), device=input_ids.device, dtype=torch.float32)
|
||||
return torch.zeros((input_ids.shape[0], 1), device=input_ids.device, dtype=torch.float32)
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor | OmniOutput, sampling_metadata: Any = None) -> None:
|
||||
return None
|
||||
|
||||
@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,
|
||||
**kwargs: Any,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Decode codec codes into audio waveform.
|
||||
|
||||
input_ids layout: [codec_context_frames, *flat_codes]
|
||||
where flat_codes is codebook-major [q*F].
|
||||
"""
|
||||
tok = self._ensure_speech_tokenizer_loaded()
|
||||
assert self._num_quantizers is not None
|
||||
assert self._output_sample_rate is not None
|
||||
|
||||
sr_val = self._output_sample_rate
|
||||
empty_ret = (
|
||||
torch.zeros((0,), dtype=torch.float32),
|
||||
torch.tensor(sr_val, dtype=torch.int32),
|
||||
)
|
||||
|
||||
if input_ids is None:
|
||||
return empty_ret
|
||||
|
||||
q = int(self._num_quantizers)
|
||||
ids = input_ids.reshape(-1).to(dtype=torch.long)
|
||||
n_tokens = ids.numel()
|
||||
|
||||
if n_tokens == 0:
|
||||
return empty_ret
|
||||
|
||||
# input_ids[0] = codec_context_frames (prepended by stage_input_processor).
|
||||
ctx_frames = int(ids[0].item())
|
||||
ids = ids[1:]
|
||||
n_tokens = ids.numel()
|
||||
|
||||
if n_tokens == 0:
|
||||
return empty_ret
|
||||
|
||||
# Warmup / dummy_run: not divisible by num_quantizers.
|
||||
if n_tokens % q != 0:
|
||||
logger.warning(
|
||||
"Code2Wav input_ids length %d not divisible by num_quantizers %d, "
|
||||
"likely a warmup run; returning empty audio.",
|
||||
n_tokens,
|
||||
q,
|
||||
)
|
||||
return empty_ret
|
||||
|
||||
total_frames = n_tokens // q
|
||||
|
||||
# Reshape codebook-major flat [q*F] -> [q, F] -> [F, q] for SpeechTokenizer.
|
||||
codes_fq = ids.reshape(q, total_frames).transpose(0, 1).contiguous()
|
||||
|
||||
if not self._logged_codec_stats and total_frames > 1:
|
||||
self._logged_codec_stats = True
|
||||
try:
|
||||
uniq = int(torch.unique(codes_fq).numel())
|
||||
cmin = int(codes_fq.min().item())
|
||||
cmax = int(codes_fq.max().item())
|
||||
head = codes_fq[: min(2, total_frames), : min(8, q)].cpu().tolist()
|
||||
logger.info(
|
||||
"Code2Wav codec: frames=%d q=%d uniq=%d range=[%d,%d] head=%s",
|
||||
total_frames,
|
||||
q,
|
||||
uniq,
|
||||
cmin,
|
||||
cmax,
|
||||
head,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
wavs, sr = tok.decode({"audio_codes": codes_fq})
|
||||
if not wavs:
|
||||
raise ValueError("SpeechTokenizer code2wav produced empty waveform list.")
|
||||
audio_np = wavs[0].astype(np.float32, copy=False)
|
||||
|
||||
# Trim left-context waveform samples (streaming sliding window).
|
||||
if ctx_frames > 0:
|
||||
upsample = self._decode_upsample_rate
|
||||
if upsample is None or upsample <= 0:
|
||||
raise ValueError(f"Invalid decode upsample rate: {upsample}")
|
||||
cut = ctx_frames * upsample
|
||||
if cut < audio_np.shape[0]:
|
||||
audio_np = audio_np[cut:]
|
||||
else:
|
||||
logger.warning(
|
||||
"Context trim %d >= decoded length %d; returning empty audio.",
|
||||
cut,
|
||||
audio_np.shape[0],
|
||||
)
|
||||
return empty_ret
|
||||
|
||||
audio_tensor = torch.from_numpy(audio_np).to(dtype=torch.float32).reshape(-1)
|
||||
sr_tensor = torch.tensor(int(sr), dtype=torch.int32)
|
||||
return audio_tensor, sr_tensor
|
||||
|
||||
def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput:
|
||||
if isinstance(model_outputs, OmniOutput):
|
||||
return model_outputs
|
||||
|
||||
if not (isinstance(model_outputs, tuple) and len(model_outputs) == 2):
|
||||
raise TypeError(f"Qwen3TTSCode2Wav expected (audio_tensor, sr) outputs, got {type(model_outputs)}")
|
||||
|
||||
audio_tensor, sr = model_outputs
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={
|
||||
"model_outputs": audio_tensor,
|
||||
"sr": sr,
|
||||
},
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
# SpeechTokenizer weights live under `speech_tokenizer/` and are loaded
|
||||
# lazily from that directory. Ignore main checkpoint weights.
|
||||
return set()
|
||||
@@ -0,0 +1,495 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.config.vllm import set_current_vllm_config
|
||||
from vllm.forward_context import set_forward_context
|
||||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||||
from vllm.model_executor.model_loader.weight_utils import (
|
||||
default_weight_loader,
|
||||
maybe_remap_kv_scale_name,
|
||||
)
|
||||
from vllm.model_executor.models.qwen3 import Qwen3DecoderLayer
|
||||
from vllm.model_executor.models.utils import is_pp_missing_parameter
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheGroupSpec, KVCacheSpec, KVCacheTensor
|
||||
from vllm.v1.worker.gpu import attn_utils
|
||||
|
||||
from .configuration_qwen3_tts import Qwen3TTSTalkerCodePredictorConfig, Qwen3TTSTalkerConfig
|
||||
|
||||
|
||||
class _LocalPredictorKVCache:
|
||||
"""Minimal local KV cache + attention metadata for running
|
||||
code_predictor inside one worker (independent of engine KV)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
vllm_config: VllmConfig,
|
||||
max_seq_len: int,
|
||||
max_batch_size: int,
|
||||
device: torch.device,
|
||||
) -> None:
|
||||
self.vllm_config = vllm_config
|
||||
self.device = device
|
||||
|
||||
# Collect attention layers registered in this vllm_config.
|
||||
kv_cache_spec_by_layer = attn_utils.get_kv_cache_spec(vllm_config)
|
||||
if not kv_cache_spec_by_layer:
|
||||
raise RuntimeError("Local predictor KVCache requires vLLM Attention layers to be registered.")
|
||||
|
||||
# We only need enough blocks for a tiny per-frame sequence (<= max_seq_len).
|
||||
any_spec = next(iter(kv_cache_spec_by_layer.values()))
|
||||
block_size = int(any_spec.block_size)
|
||||
blocks_per_seq = (int(max_seq_len) + block_size - 1) // block_size
|
||||
num_blocks = max(1, int(max_batch_size) * int(blocks_per_seq))
|
||||
|
||||
# Allocate per-layer KV caches (small, independent).
|
||||
kv_cache_tensors: list[KVCacheTensor] = []
|
||||
for layer_name, spec in kv_cache_spec_by_layer.items():
|
||||
kv_cache_tensors.append(KVCacheTensor(size=int(spec.page_size_bytes) * num_blocks, shared_by=[layer_name]))
|
||||
|
||||
merged_spec: KVCacheSpec = KVCacheSpec.merge(list(kv_cache_spec_by_layer.values()))
|
||||
self.kv_cache_config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=kv_cache_tensors,
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=list(kv_cache_spec_by_layer.keys()), kv_cache_spec=merged_spec)
|
||||
],
|
||||
)
|
||||
|
||||
# Init backend + bind KV cache tensors to attention modules.
|
||||
self.attn_backends, self.attn_metadata_builders = attn_utils.init_attn_backend(
|
||||
self.kv_cache_config, vllm_config, device
|
||||
)
|
||||
self.runner_kv_caches: list[torch.Tensor] = []
|
||||
attn_utils.init_kv_cache(
|
||||
self.runner_kv_caches,
|
||||
vllm_config.compilation_config.static_forward_context,
|
||||
self.kv_cache_config,
|
||||
self.attn_backends,
|
||||
device,
|
||||
)
|
||||
|
||||
# Precompute a fixed block table mapping for the maximum batch.
|
||||
self.block_size = block_size
|
||||
self.blocks_per_seq = blocks_per_seq
|
||||
self.max_batch_size = int(max_batch_size)
|
||||
|
||||
bt = torch.full((self.max_batch_size, self.blocks_per_seq), -1, dtype=torch.int32, device=device)
|
||||
for i in range(self.max_batch_size):
|
||||
for j in range(self.blocks_per_seq):
|
||||
bt[i, j] = i * self.blocks_per_seq + j
|
||||
self._block_table = bt
|
||||
|
||||
def build_attn_metadata(
|
||||
self,
|
||||
*,
|
||||
num_reqs: int,
|
||||
query_lens: torch.Tensor, # (num_reqs,) int32 on cpu
|
||||
seq_lens: torch.Tensor, # (num_reqs,) int32 on cpu
|
||||
) -> tuple[dict[str, Any], torch.Tensor, dict[str, torch.Tensor]]:
|
||||
"""Build attention metadata, positions, and slot_mapping dict.
|
||||
|
||||
Returns:
|
||||
(attn_metadata, positions, slot_mappings_by_layer)
|
||||
- attn_metadata: per-layer attention metadata for attn backends.
|
||||
- positions: (num_tokens,) position IDs on device.
|
||||
- slot_mappings_by_layer: {layer_name: slot_mapping_tensor} for
|
||||
set_forward_context so that unified_kv_cache_update can write
|
||||
the KV cache correctly.
|
||||
"""
|
||||
num_reqs = int(num_reqs)
|
||||
if num_reqs <= 0:
|
||||
return {}, torch.empty((0,), dtype=torch.int64, device=self.device), {}
|
||||
if num_reqs > self.max_batch_size:
|
||||
raise ValueError(f"num_reqs={num_reqs} exceeds local predictor max_batch_size={self.max_batch_size}")
|
||||
|
||||
query_lens_i32 = query_lens.to(dtype=torch.int32, device="cpu")
|
||||
seq_lens_i32 = seq_lens.to(dtype=torch.int32, device="cpu")
|
||||
|
||||
# query_start_loc: prefix sums of query_lens.
|
||||
qsl = torch.zeros((num_reqs + 1,), dtype=torch.int32, device="cpu")
|
||||
qsl[1:] = torch.cumsum(query_lens_i32, dim=0)
|
||||
num_tokens = int(qsl[-1].item())
|
||||
if num_tokens <= 0:
|
||||
return {}, torch.empty((0,), dtype=torch.int64, device=self.device), {}
|
||||
|
||||
# positions: for each request i, emit positions [seq_len-query_len .. seq_len-1]
|
||||
pos_list: list[torch.Tensor] = []
|
||||
for i in range(num_reqs):
|
||||
ql = int(query_lens_i32[i].item())
|
||||
sl = int(seq_lens_i32[i].item())
|
||||
start = sl - ql
|
||||
pos_list.append(torch.arange(start, sl, dtype=torch.int64))
|
||||
positions_cpu = torch.cat(pos_list, dim=0)
|
||||
|
||||
# slot_mapping: map each query token to a physical slot in the paged KV cache.
|
||||
# We allocate per-request contiguous blocks; slot = base + position.
|
||||
slot_mapping = torch.empty((num_tokens,), dtype=torch.int64, device="cpu")
|
||||
cursor = 0
|
||||
for i in range(num_reqs):
|
||||
ql = int(query_lens_i32[i].item())
|
||||
sl = int(seq_lens_i32[i].item())
|
||||
start = sl - ql
|
||||
for p in range(start, sl):
|
||||
block_idx = p // self.block_size
|
||||
offset = p % self.block_size
|
||||
block_id = int(self._block_table[i, block_idx].item())
|
||||
slot_mapping[cursor] = block_id * self.block_size + offset
|
||||
cursor += 1
|
||||
|
||||
max_seq_len = int(seq_lens_i32[:num_reqs].max().item())
|
||||
query_start_loc_gpu = qsl.to(device=self.device)
|
||||
seq_lens_gpu = seq_lens_i32.to(device=self.device)
|
||||
block_table = self._block_table[:num_reqs].contiguous()
|
||||
slot_mapping_gpu = slot_mapping.to(device=self.device)
|
||||
|
||||
attn_metadata = attn_utils.build_attn_metadata(
|
||||
self.attn_metadata_builders,
|
||||
num_reqs=num_reqs,
|
||||
num_tokens=num_tokens,
|
||||
query_start_loc_gpu=query_start_loc_gpu,
|
||||
query_start_loc_cpu=qsl,
|
||||
seq_lens=seq_lens_gpu,
|
||||
max_seq_len=max_seq_len,
|
||||
block_tables=[block_table],
|
||||
slot_mappings=[slot_mapping_gpu],
|
||||
kv_cache_config=self.kv_cache_config,
|
||||
)
|
||||
|
||||
# Build slot_mappings_by_layer for set_forward_context.
|
||||
# Fix for vllm 0.15.0
|
||||
slot_mappings_by_layer: dict[str, torch.Tensor] = {}
|
||||
for kv_cache_group in self.kv_cache_config.kv_cache_groups:
|
||||
for layer_name in kv_cache_group.layer_names:
|
||||
slot_mappings_by_layer[layer_name] = slot_mapping_gpu
|
||||
|
||||
return attn_metadata, positions_cpu.to(device=self.device), slot_mappings_by_layer
|
||||
|
||||
|
||||
class Qwen3TTSTalkerCodePredictorModelVLLM(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: Qwen3TTSTalkerCodePredictorConfig,
|
||||
*,
|
||||
talker_hidden_size: int | None = None,
|
||||
cache_config=None,
|
||||
quant_config=None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
|
||||
self.layers = nn.ModuleList(
|
||||
[
|
||||
Qwen3DecoderLayer(
|
||||
config, cache_config=cache_config, quant_config=quant_config, prefix=f"{prefix}.layers.{i}"
|
||||
)
|
||||
for i in range(config.num_hidden_layers)
|
||||
]
|
||||
)
|
||||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
# Official code_predictor uses one embedding table per residual group.
|
||||
# Some Qwen3-TTS checkpoints store codec embeddings in the talker hidden
|
||||
# space, even when `code_predictor_config.hidden_size` is smaller.
|
||||
# We keep the embedding dim aligned with the checkpoint and project down
|
||||
# via `small_to_mtp_projection` in the wrapper module.
|
||||
emb_dim = int(talker_hidden_size) if talker_hidden_size is not None else int(config.hidden_size)
|
||||
self.codec_embedding = nn.ModuleList(
|
||||
[nn.Embedding(config.vocab_size, emb_dim) for _ in range(config.num_code_groups - 1)]
|
||||
)
|
||||
|
||||
def get_input_embeddings(self) -> nn.ModuleList:
|
||||
return self.codec_embedding
|
||||
|
||||
def forward(self, positions: torch.Tensor, inputs_embeds: torch.Tensor) -> torch.Tensor:
|
||||
# Token-major: [num_tokens, hidden]
|
||||
hidden_states = inputs_embeds
|
||||
residual = None
|
||||
for layer in self.layers:
|
||||
hidden_states, residual = layer(positions, hidden_states, residual)
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
return hidden_states
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
# Match vLLM Qwen2/Qwen3 packing conventions: q_proj/k_proj/v_proj -> qkv_proj,
|
||||
# gate_proj/up_proj -> gate_up_proj.
|
||||
stacked_params_mapping = [
|
||||
("qkv_proj", "q_proj", "q"),
|
||||
("qkv_proj", "k_proj", "k"),
|
||||
("qkv_proj", "v_proj", "v"),
|
||||
("gate_up_proj", "gate_proj", 0),
|
||||
("gate_up_proj", "up_proj", 1),
|
||||
]
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
loaded_params: set[str] = set()
|
||||
for name, loaded_weight in weights:
|
||||
if "rotary_emb.inv_freq" in name:
|
||||
continue
|
||||
if self.quant_config is not None and (scale_name := self.quant_config.get_cache_scale(name)):
|
||||
param = params_dict[scale_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
loaded_weight = loaded_weight if loaded_weight.dim() == 0 else loaded_weight[0]
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(scale_name)
|
||||
continue
|
||||
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
continue
|
||||
mapped = name.replace(weight_name, param_name)
|
||||
if mapped.endswith(".bias") and mapped not in params_dict:
|
||||
continue
|
||||
if is_pp_missing_parameter(mapped, self):
|
||||
continue
|
||||
if mapped.endswith("scale"):
|
||||
mapped = maybe_remap_kv_scale_name(mapped, params_dict)
|
||||
if mapped is None:
|
||||
continue
|
||||
param = params_dict.get(mapped)
|
||||
if param is None:
|
||||
continue
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
if weight_loader == default_weight_loader:
|
||||
weight_loader(param, loaded_weight)
|
||||
else:
|
||||
weight_loader(param, loaded_weight, shard_id)
|
||||
loaded_params.add(mapped)
|
||||
break
|
||||
else:
|
||||
mapped = maybe_remap_kv_scale_name(name, params_dict)
|
||||
if mapped is None:
|
||||
continue
|
||||
if name.endswith(".bias") and mapped not in params_dict:
|
||||
continue
|
||||
if is_pp_missing_parameter(mapped, self):
|
||||
continue
|
||||
param = params_dict.get(mapped)
|
||||
if param is None:
|
||||
continue
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(mapped)
|
||||
return loaded_params
|
||||
|
||||
|
||||
class Qwen3TTSTalkerCodePredictorForConditionalGenerationVLLM(nn.Module):
|
||||
"""vLLM-native code_predictor used by the AR talker (residual codebooks)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
vllm_config: VllmConfig,
|
||||
config: Qwen3TTSTalkerCodePredictorConfig,
|
||||
talker_config: Qwen3TTSTalkerConfig,
|
||||
prefix: str = "code_predictor",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._vllm_config = vllm_config
|
||||
self.config = config
|
||||
self.talker_config = talker_config
|
||||
|
||||
# Keep module/weight names aligned with official checkpoint (talker.code_predictor.model.*).
|
||||
self.model = Qwen3TTSTalkerCodePredictorModelVLLM(
|
||||
config,
|
||||
talker_hidden_size=int(talker_config.hidden_size),
|
||||
cache_config=vllm_config.cache_config,
|
||||
quant_config=vllm_config.quant_config,
|
||||
prefix=f"{prefix}.model",
|
||||
)
|
||||
|
||||
# One head per residual group.
|
||||
self.lm_head = nn.ModuleList(
|
||||
[nn.Linear(config.hidden_size, config.vocab_size, bias=False) for _ in range(config.num_code_groups - 1)]
|
||||
)
|
||||
|
||||
if config.hidden_size != talker_config.hidden_size:
|
||||
self.small_to_mtp_projection = nn.Linear(talker_config.hidden_size, config.hidden_size, bias=True)
|
||||
else:
|
||||
self.small_to_mtp_projection = nn.Identity()
|
||||
|
||||
self._kv_cache: _LocalPredictorKVCache | None = None
|
||||
|
||||
def get_input_embeddings(self) -> nn.ModuleList:
|
||||
return self.model.get_input_embeddings()
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
# Ensure all vLLM custom layers consult the predictor vllm_config
|
||||
# (esp. for Attention static_forward_context).
|
||||
with set_current_vllm_config(self._vllm_config):
|
||||
loaded: set[str] = set()
|
||||
model_weights: list[tuple[str, torch.Tensor]] = []
|
||||
other_weights: list[tuple[str, torch.Tensor]] = []
|
||||
for name, w in weights:
|
||||
if name.startswith("model."):
|
||||
model_weights.append((name[len("model.") :], w))
|
||||
else:
|
||||
other_weights.append((name, w))
|
||||
|
||||
loaded_model = self.model.load_weights(model_weights)
|
||||
loaded |= {f"model.{n}" for n in loaded_model}
|
||||
|
||||
params = dict(self.named_parameters(remove_duplicate=False))
|
||||
for name, w in other_weights:
|
||||
if name not in params:
|
||||
continue
|
||||
default_weight_loader(params[name], w)
|
||||
loaded.add(name)
|
||||
return loaded
|
||||
|
||||
def _maybe_init_kv_cache(self, device: torch.device) -> None:
|
||||
if self._kv_cache is not None:
|
||||
return
|
||||
max_seq_len = int(getattr(self.config, "num_code_groups", 16) or 16)
|
||||
# Upper bound on batch size: vLLM scheduler max_num_seqs (fallback 8).
|
||||
max_batch = int(getattr(self._vllm_config.scheduler_config, "max_num_seqs", 8) or 8)
|
||||
max_batch = max(1, max_batch)
|
||||
self._kv_cache = _LocalPredictorKVCache(
|
||||
vllm_config=self._vllm_config,
|
||||
max_seq_len=max_seq_len,
|
||||
max_batch_size=max_batch,
|
||||
device=device,
|
||||
)
|
||||
|
||||
@torch.inference_mode()
|
||||
def reset_cache(self) -> None:
|
||||
# We reuse a fixed kv cache buffer and overwrite starting at slot 0.
|
||||
# No action required here (seq_lens controls what is read).
|
||||
return
|
||||
|
||||
@torch.inference_mode()
|
||||
def prefill_logits(self, inputs_embeds: torch.Tensor) -> torch.Tensor:
|
||||
"""Prefill with 2 tokens: [past_hidden, layer0_embed]. Returns logits for residual group 0."""
|
||||
self._maybe_init_kv_cache(inputs_embeds.device)
|
||||
assert self._kv_cache is not None
|
||||
|
||||
bsz = int(inputs_embeds.shape[0])
|
||||
qlen = 2
|
||||
# Flatten to token-major.
|
||||
hs = inputs_embeds.to(dtype=torch.bfloat16).reshape(bsz * qlen, -1)
|
||||
hs = self.small_to_mtp_projection(hs)
|
||||
|
||||
query_lens = torch.full((bsz,), qlen, dtype=torch.int32)
|
||||
seq_lens = query_lens.clone()
|
||||
attn_metadata, positions, slot_mappings = self._kv_cache.build_attn_metadata(
|
||||
num_reqs=bsz, query_lens=query_lens, seq_lens=seq_lens
|
||||
)
|
||||
|
||||
with (
|
||||
set_current_vllm_config(self._vllm_config),
|
||||
set_forward_context(
|
||||
attn_metadata,
|
||||
self._vllm_config,
|
||||
num_tokens=int(hs.shape[0]),
|
||||
slot_mapping=slot_mappings,
|
||||
),
|
||||
):
|
||||
out = self.model(positions=positions, inputs_embeds=hs)
|
||||
|
||||
# Gather last token per request.
|
||||
last_idx = torch.arange(qlen - 1, bsz * qlen, step=qlen, device=out.device, dtype=torch.long)
|
||||
last_h = out.index_select(0, last_idx)
|
||||
logits = self.lm_head[0](last_h)
|
||||
return logits
|
||||
|
||||
@torch.inference_mode()
|
||||
def decode_logits(self, input_ids: torch.Tensor, *, generation_step: int, past_seq_len: int) -> torch.Tensor:
|
||||
"""Decode one new token for residual group `generation_step` (1..Q-1)."""
|
||||
self._maybe_init_kv_cache(input_ids.device)
|
||||
assert self._kv_cache is not None
|
||||
bsz = int(input_ids.shape[0])
|
||||
if generation_step <= 0:
|
||||
raise ValueError("generation_step must be >= 1 for decode_logits")
|
||||
|
||||
embed_idx = generation_step - 1
|
||||
hs = self.model.get_input_embeddings()[embed_idx](input_ids.to(dtype=torch.long).reshape(bsz, 1))
|
||||
hs = self.small_to_mtp_projection(hs.reshape(bsz, -1))
|
||||
|
||||
query_lens = torch.ones((bsz,), dtype=torch.int32)
|
||||
seq_lens = torch.full((bsz,), int(past_seq_len) + 1, dtype=torch.int32)
|
||||
attn_metadata, positions, slot_mappings = self._kv_cache.build_attn_metadata(
|
||||
num_reqs=bsz, query_lens=query_lens, seq_lens=seq_lens
|
||||
)
|
||||
|
||||
with (
|
||||
set_current_vllm_config(self._vllm_config),
|
||||
set_forward_context(
|
||||
attn_metadata,
|
||||
self._vllm_config,
|
||||
num_tokens=int(hs.shape[0]),
|
||||
slot_mapping=slot_mappings,
|
||||
),
|
||||
):
|
||||
out = self.model(positions=positions, inputs_embeds=hs)
|
||||
|
||||
logits = self.lm_head[generation_step](out)
|
||||
return logits
|
||||
|
||||
@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 = 1.0,
|
||||
top_k: int = 50,
|
||||
top_p: float = 1.0,
|
||||
) -> torch.Tensor:
|
||||
"""Full autoregressive prediction of residual codebooks 1..Q-1.
|
||||
|
||||
Args:
|
||||
layer0_code: [B, 1] first-layer codec token ids.
|
||||
layer0_embed: [B, 1, H] embedding of layer0_code.
|
||||
last_talker_hidden: [B, 1, H] hidden state from the talker.
|
||||
do_sample: whether to sample or take argmax.
|
||||
temperature: sampling temperature.
|
||||
top_k: top-k filtering.
|
||||
top_p: top-p (nucleus) filtering.
|
||||
|
||||
Returns:
|
||||
audio_codes: [B, Q] all codebook tokens (layer0 + residuals).
|
||||
"""
|
||||
bsz = int(layer0_code.shape[0])
|
||||
num_groups = int(self.config.num_code_groups)
|
||||
max_steps = num_groups - 1
|
||||
|
||||
# Reset KV cache for a fresh sequence.
|
||||
self.reset_cache()
|
||||
|
||||
# Prefill: feed [last_talker_hidden, layer0_embed] → logits for group 1.
|
||||
prefill_input = torch.cat([last_talker_hidden, layer0_embed], dim=1) # [B, 2, H]
|
||||
logits = self.prefill_logits(prefill_input) # [B, vocab]
|
||||
|
||||
all_codes = [layer0_code.reshape(bsz, 1)]
|
||||
past_seq_len = 2
|
||||
|
||||
for step in range(1, num_groups):
|
||||
# Sample or argmax from logits.
|
||||
if do_sample and temperature > 0:
|
||||
scaled = logits / temperature
|
||||
if top_k > 0:
|
||||
topk_vals, _ = scaled.topk(top_k, dim=-1)
|
||||
scaled = scaled.masked_fill(scaled < topk_vals[:, -1:], float("-inf"))
|
||||
probs = torch.softmax(scaled, dim=-1)
|
||||
next_ids = torch.multinomial(probs, num_samples=1) # [B, 1]
|
||||
else:
|
||||
next_ids = logits.argmax(dim=-1, keepdim=True) # [B, 1]
|
||||
all_codes.append(next_ids)
|
||||
|
||||
# If not the last step, decode one more token.
|
||||
if step < max_steps:
|
||||
logits = self.decode_logits(
|
||||
next_ids.reshape(bsz),
|
||||
generation_step=step,
|
||||
past_seq_len=past_seq_len,
|
||||
)
|
||||
past_seq_len += 1
|
||||
|
||||
return torch.cat(all_codes, dim=1) # [B, Q]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -80,16 +80,21 @@ class Qwen3TTSTokenizer:
|
||||
"""
|
||||
inst = cls()
|
||||
|
||||
AutoConfig.register("qwen3_tts_tokenizer_25hz", Qwen3TTSTokenizerV1Config)
|
||||
AutoModel.register(Qwen3TTSTokenizerV1Config, Qwen3TTSTokenizerV1Model)
|
||||
load_feature_extractor = bool(kwargs.pop("load_feature_extractor", True))
|
||||
|
||||
AutoConfig.register("qwen3_tts_tokenizer_12hz", Qwen3TTSTokenizerV2Config)
|
||||
AutoModel.register(Qwen3TTSTokenizerV2Config, Qwen3TTSTokenizerV2Model)
|
||||
|
||||
inst.feature_extractor = AutoFeatureExtractor.from_pretrained(pretrained_model_name_or_path)
|
||||
AutoConfig.register("qwen3_tts_tokenizer_25hz", Qwen3TTSTokenizerV1Config)
|
||||
AutoModel.register(Qwen3TTSTokenizerV1Config, Qwen3TTSTokenizerV1Model)
|
||||
|
||||
inst.model = AutoModel.from_pretrained(pretrained_model_name_or_path, **kwargs)
|
||||
inst.config = inst.model.config
|
||||
|
||||
inst.feature_extractor = (
|
||||
AutoFeatureExtractor.from_pretrained(pretrained_model_name_or_path) if load_feature_extractor else None
|
||||
)
|
||||
|
||||
inst.device = getattr(inst.model, "device", None)
|
||||
if inst.device is None:
|
||||
# fallback: infer from first parameter device
|
||||
@@ -212,12 +217,7 @@ class Qwen3TTSTokenizer:
|
||||
audios: AudioInput,
|
||||
sr: int | None = None,
|
||||
return_dict: bool = True,
|
||||
) -> (
|
||||
Qwen3TTSTokenizerV1EncoderOutput
|
||||
| Qwen3TTSTokenizerV2EncoderOutput
|
||||
| tuple[list[torch.Tensor], list[torch.Tensor] | None, list[torch.Tensor] | None]
|
||||
| tuple[list[torch.Tensor]]
|
||||
):
|
||||
) -> Qwen3TTSTokenizerV1EncoderOutput | Qwen3TTSTokenizerV2EncoderOutput | tuple:
|
||||
"""
|
||||
Batch-encode audio into discrete codes (and optional conditioning, depending on 25Hz/12Hz).
|
||||
|
||||
|
||||
@@ -50,8 +50,18 @@ _OMNI_MODELS = {
|
||||
),
|
||||
"Qwen3TTSForConditionalGeneration": (
|
||||
"qwen3_tts",
|
||||
"qwen3_tts_talker",
|
||||
"Qwen3TTSTalkerForConditionalGeneration",
|
||||
),
|
||||
"Qwen3TTSTalkerForConditionalGeneration": (
|
||||
"qwen3_tts",
|
||||
"Qwen3TTSModelForGeneration",
|
||||
"qwen3_tts_talker",
|
||||
"Qwen3TTSTalkerForConditionalGeneration",
|
||||
),
|
||||
"Qwen3TTSCode2Wav": (
|
||||
"qwen3_tts",
|
||||
"qwen3_tts_code2wav",
|
||||
"Qwen3TTSCode2Wav",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,101 @@
|
||||
async_chunk: true
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
stage_type: llm # Use llm stage type to launch OmniLLM
|
||||
stage_type: llm
|
||||
runtime:
|
||||
devices: "0"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: qwen3_tts
|
||||
model_arch: Qwen3TTSForConditionalGeneration
|
||||
model_arch: Qwen3TTSTalkerForConditionalGeneration
|
||||
# Force stage-specific registered architecture.
|
||||
hf_overrides:
|
||||
architectures: [Qwen3TTSTalkerForConditionalGeneration]
|
||||
worker_type: ar
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
enforce_eager: false
|
||||
trust_remote_code: true
|
||||
async_scheduling: false
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: latent
|
||||
gpu_memory_utilization: 0.3
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 512
|
||||
max_model_len: 4096
|
||||
custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_tts.talker2code2wav_async_chunk
|
||||
# Use named connector to apply runtime.connectors.extra.
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.9
|
||||
top_k: 50
|
||||
max_tokens: 4096
|
||||
seed: 42
|
||||
detokenize: false
|
||||
repetition_penalty: 1.05
|
||||
stop_token_ids: [2150]
|
||||
|
||||
- stage_id: 1
|
||||
stage_type: llm
|
||||
runtime:
|
||||
devices: "0"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: code2wav
|
||||
model_arch: Qwen3TTSCode2Wav
|
||||
# Force stage-specific registered architecture.
|
||||
hf_overrides:
|
||||
architectures: [Qwen3TTSCode2Wav]
|
||||
worker_type: generation
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
async_scheduling: false
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: audio # Final output: audio waveform
|
||||
gpu_memory_utilization: 0.1
|
||||
engine_output_type: audio
|
||||
gpu_memory_utilization: 0.2
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 1000000
|
||||
|
||||
# Must be divisible by num_code_groups and cover (left_context + chunk).
|
||||
max_num_batched_tokens: 8192
|
||||
# async_chunk appends windows per step; max_model_len must cover accumulated stream.
|
||||
max_model_len: 32768
|
||||
engine_input_source: [0]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
# Distributed connector configuration
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 65536
|
||||
seed: 42
|
||||
detokenize: true
|
||||
repetition_penalty: 1.0
|
||||
|
||||
runtime:
|
||||
enabled: true
|
||||
defaults:
|
||||
window_size: -1
|
||||
max_inflight: 1
|
||||
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
# Frame-aligned codec streaming transport.
|
||||
codec_streaming: true
|
||||
# Connector polling / timeout (unit: loop count, sleep interval in seconds).
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
connector_get_max_wait: 300
|
||||
# Align with Omni: small chunks with sufficient context overlap.
|
||||
codec_chunk_frames: 25
|
||||
codec_left_context_frames: 25
|
||||
|
||||
edges:
|
||||
- from: 0
|
||||
to: 1
|
||||
window_size: -1
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Stage input processor for Qwen3-TTS: Talker -> Code2Wav."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def _extract_last_frame(pooling_output: dict[str, Any]) -> torch.Tensor | None:
|
||||
audio_codes = pooling_output.get("audio_codes")
|
||||
if not isinstance(audio_codes, torch.Tensor) or audio_codes.numel() == 0:
|
||||
return None
|
||||
if audio_codes.ndim == 2:
|
||||
frame = audio_codes[-1]
|
||||
if frame.numel() == 0 or not bool(frame.any().item()):
|
||||
return None
|
||||
return frame.to(torch.long).reshape(-1)
|
||||
if audio_codes.ndim == 1:
|
||||
return audio_codes.to(torch.long).reshape(-1)
|
||||
raise ValueError(f"Invalid audio_codes shape for Qwen3-TTS async_chunk: {tuple(audio_codes.shape)}")
|
||||
|
||||
|
||||
def talker2code2wav_async_chunk(
|
||||
transfer_manager: Any,
|
||||
pooling_output: dict[str, Any],
|
||||
request: Any,
|
||||
) -> dict[str, Any] | None:
|
||||
if not isinstance(pooling_output, dict):
|
||||
return None
|
||||
|
||||
request_id = request.external_req_id
|
||||
|
||||
connector = getattr(transfer_manager, "connector", None)
|
||||
raw_cfg = getattr(connector, "config", {}) or {}
|
||||
cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, dict) else {}
|
||||
chunk_size = int(cfg.get("codec_chunk_frames", 25))
|
||||
left_context_size = int(cfg.get("codec_left_context_frames", 25))
|
||||
if chunk_size <= 0 or left_context_size < 0:
|
||||
raise ValueError(
|
||||
f"Invalid codec chunk config: codec_chunk_frames={chunk_size}, "
|
||||
f"codec_left_context_frames={left_context_size}"
|
||||
)
|
||||
|
||||
finished = bool(request.is_finished())
|
||||
|
||||
frame = _extract_last_frame(pooling_output)
|
||||
if frame is not None:
|
||||
codec_codes = frame.cpu().tolist()
|
||||
transfer_manager.code_prompt_token_ids[request_id].append(codec_codes)
|
||||
|
||||
length = len(transfer_manager.code_prompt_token_ids[request_id])
|
||||
chunk_length = length % chunk_size
|
||||
|
||||
if chunk_length != 0 and not finished:
|
||||
return None
|
||||
|
||||
context_length = chunk_length if chunk_length != 0 else chunk_size
|
||||
|
||||
if length <= 0:
|
||||
return {
|
||||
"code_predictor_codes": [],
|
||||
"finished": torch.tensor(bool(finished), dtype=torch.bool),
|
||||
}
|
||||
|
||||
end_index = min(length, left_context_size + context_length)
|
||||
ctx_frames = max(0, int(end_index - context_length))
|
||||
window_frames = transfer_manager.code_prompt_token_ids[request_id][-end_index:]
|
||||
|
||||
# Pack context + chunk into codebook-major flat codes for adapter.
|
||||
code_predictor_codes = torch.tensor(window_frames).transpose(0, 1).reshape(-1).tolist()
|
||||
|
||||
# Build final prompt_token_ids with ctx_frames header for Qwen3-TTS Code2Wav.
|
||||
# The model expects input_ids layout: [ctx_frames, *flat_codes].
|
||||
return {
|
||||
"code_predictor_codes": [int(ctx_frames)] + code_predictor_codes,
|
||||
"finished": torch.tensor(bool(finished), dtype=torch.bool),
|
||||
}
|
||||
@@ -1,22 +1,92 @@
|
||||
async_chunk: true
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
stage_type: llm # Use llm stage type to launch OmniLLM
|
||||
stage_type: llm
|
||||
runtime:
|
||||
devices: "0"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: qwen3_tts
|
||||
model_arch: Qwen3TTSForConditionalGeneration
|
||||
model_arch: Qwen3TTSTalkerForConditionalGeneration
|
||||
hf_overrides:
|
||||
architectures: [Qwen3TTSTalkerForConditionalGeneration]
|
||||
worker_type: ar
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
async_scheduling: false
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: latent
|
||||
gpu_memory_utilization: 0.3
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 512
|
||||
max_model_len: 4096
|
||||
custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_tts.talker2code2wav_async_chunk
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.9
|
||||
top_k: 50
|
||||
max_tokens: 4096
|
||||
seed: 42
|
||||
detokenize: false
|
||||
repetition_penalty: 1.05
|
||||
stop_token_ids: [2150]
|
||||
|
||||
- stage_id: 1
|
||||
stage_type: llm
|
||||
runtime:
|
||||
devices: "0"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: code2wav
|
||||
model_arch: Qwen3TTSCode2Wav
|
||||
hf_overrides:
|
||||
architectures: [Qwen3TTSCode2Wav]
|
||||
worker_type: generation
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
async_scheduling: false
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: audio # Final output: audio waveform
|
||||
gpu_memory_utilization: 0.1
|
||||
engine_output_type: audio
|
||||
gpu_memory_utilization: 0.2
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 1000000
|
||||
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 32768
|
||||
engine_input_source: [0]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 65536
|
||||
seed: 42
|
||||
detokenize: true
|
||||
repetition_penalty: 1.0
|
||||
|
||||
runtime:
|
||||
enabled: true
|
||||
defaults:
|
||||
window_size: -1
|
||||
max_inflight: 1
|
||||
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
connector_get_max_wait: 300
|
||||
codec_chunk_frames: 25
|
||||
codec_left_context_frames: 25
|
||||
|
||||
edges:
|
||||
- from: 0
|
||||
to: 1
|
||||
window_size: -1
|
||||
@@ -238,9 +238,12 @@ class NPUGenerationModelRunner(OmniNPUModelRunner):
|
||||
pooler_output.append(mm_payload)
|
||||
else:
|
||||
raise RuntimeError("Unsupported diffusion output type")
|
||||
# [Omni] Copy req_id mappings to avoid async scheduling mutation.
|
||||
req_ids_output_copy = self.input_batch.req_ids.copy()
|
||||
req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy()
|
||||
output = OmniModelRunnerOutput(
|
||||
req_ids=self.input_batch.req_ids,
|
||||
req_id_to_index=self.input_batch.req_id_to_index,
|
||||
req_ids=req_ids_output_copy,
|
||||
req_id_to_index=req_id_to_index_output_copy,
|
||||
sampled_token_ids=[],
|
||||
logprobs=None,
|
||||
prompt_logprobs_dict={},
|
||||
|
||||
@@ -380,9 +380,12 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
|
||||
pooler_output.append(mm_payload)
|
||||
else:
|
||||
raise RuntimeError("Unsupported diffusion output type")
|
||||
# [Omni] Copy req_id mappings to avoid async scheduling mutation.
|
||||
req_ids_output_copy = self.input_batch.req_ids.copy()
|
||||
req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy()
|
||||
output = OmniModelRunnerOutput(
|
||||
req_ids=self.input_batch.req_ids,
|
||||
req_id_to_index=self.input_batch.req_id_to_index,
|
||||
req_ids=req_ids_output_copy,
|
||||
req_id_to_index=req_id_to_index_output_copy,
|
||||
sampled_token_ids=[],
|
||||
logprobs=None,
|
||||
prompt_logprobs_dict={},
|
||||
|
||||
@@ -70,16 +70,24 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
@instrument(span_name="Loading (GPU)")
|
||||
def load_model(self, *args, **kwargs) -> None:
|
||||
super().load_model(*args, **kwargs)
|
||||
|
||||
# TODO move this model specific logic to a separate class
|
||||
if hasattr(self.model, "talker_mtp") and self.model.talker is not None:
|
||||
self.talker_mtp = self.model.talker_mtp
|
||||
# TTS model IS the talker (no .talker sub-attr); use getattr to support both Omni and TTS.
|
||||
talker_mtp = getattr(self.model, "talker_mtp", None)
|
||||
if talker_mtp is not None:
|
||||
self.talker_mtp = talker_mtp # type: ignore[assignment]
|
||||
cudagraph_mode = self.compilation_config.cudagraph_mode
|
||||
assert cudagraph_mode is not None
|
||||
if cudagraph_mode.has_full_cudagraphs():
|
||||
self.talker_mtp = CUDAGraphWrapper(
|
||||
self.model.talker_mtp, self.vllm_config, runtime_mode=CUDAGraphMode.FULL
|
||||
)
|
||||
hidden_size = self.model_config.hf_config.talker_config.text_config.hidden_size
|
||||
# Only wrap talker_mtp in CUDAGraphWrapper for Omni models that
|
||||
# have a separate .talker sub-module. TTS models' code predictor
|
||||
# has internal AR loops / torch.multinomial — not graph-safe.
|
||||
has_separate_talker = getattr(self.model, "talker", None) is not None
|
||||
if cudagraph_mode.has_full_cudagraphs() and has_separate_talker:
|
||||
self.talker_mtp = CUDAGraphWrapper(talker_mtp, self.vllm_config, runtime_mode=CUDAGraphMode.FULL)
|
||||
# TTS exposes mtp_hidden_size; Omni uses hf_text_config.hidden_size.
|
||||
hidden_size = int(
|
||||
getattr(self.model, "mtp_hidden_size", 0) or getattr(self.model_config.hf_text_config, "hidden_size")
|
||||
)
|
||||
max_batch_size = max(self.max_num_reqs, self.compilation_config.max_cudagraph_capture_size)
|
||||
self.talker_mtp_input_ids = self._make_buffer(max_batch_size, dtype=torch.int32)
|
||||
self.talker_mtp_inputs_embeds = self._make_buffer(
|
||||
@@ -644,6 +652,11 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
input_ids = None
|
||||
inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded]
|
||||
model_kwargs = self._init_model_kwargs()
|
||||
elif getattr(getattr(self, "model", None), "has_preprocess", False):
|
||||
# Capture CUDA graph with inputs_embeds path so replay reads
|
||||
# from the same buffer that _preprocess writes into.
|
||||
input_ids = self.input_ids.gpu[:num_tokens_padded]
|
||||
inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded]
|
||||
else:
|
||||
input_ids = self.input_ids.gpu[:num_tokens_padded]
|
||||
inputs_embeds = None
|
||||
@@ -986,6 +999,11 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
|
||||
model_kwargs = self._init_model_kwargs()
|
||||
input_ids = self.input_ids.gpu[:num_input_tokens]
|
||||
elif getattr(self.model, "has_preprocess", False):
|
||||
# Use pre-allocated buffer for CUDA graph compatibility.
|
||||
input_ids = self.input_ids.gpu[:num_input_tokens]
|
||||
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
|
||||
model_kwargs = self._init_model_kwargs()
|
||||
else:
|
||||
# For text-only models, we use token ids as input.
|
||||
# While it is possible to use embeddings as input just like the
|
||||
@@ -1047,9 +1065,16 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
span_len = int(e) - int(s)
|
||||
|
||||
# call the custom process function
|
||||
embed_slice = inputs_embeds[s:e] if inputs_embeds is not None else None
|
||||
req_input_ids, req_embeds, update_dict = self.model.preprocess(
|
||||
input_ids=input_ids[s:e], input_embeds=inputs_embeds[s:e], **req_infos
|
||||
input_ids=input_ids[s:e], input_embeds=embed_slice, **req_infos
|
||||
)
|
||||
if inputs_embeds is None:
|
||||
inputs_embeds = torch.empty(
|
||||
(input_ids.shape[0], req_embeds.shape[-1]),
|
||||
device=req_embeds.device,
|
||||
dtype=req_embeds.dtype,
|
||||
)
|
||||
|
||||
if hasattr(self.model, "talker_mtp") and span_len == 1:
|
||||
last_talker_hidden, text_step = update_dict.pop("mtp_inputs")
|
||||
@@ -1093,6 +1118,9 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
max_num_scheduled_tokens=1,
|
||||
use_cascade_attn=False,
|
||||
)
|
||||
# Force eager for unwrapped code predictors (AR loops / multinomial).
|
||||
if not isinstance(self.talker_mtp, CUDAGraphWrapper):
|
||||
_cudagraph_mode = CUDAGraphMode.NONE
|
||||
num_tokens_padded = batch_desc.num_tokens
|
||||
req_input_ids = self.talker_mtp_input_ids.gpu[:num_tokens_padded]
|
||||
req_embeds = self.talker_mtp_inputs_embeds.gpu[:num_tokens_padded]
|
||||
@@ -1104,11 +1132,12 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
req_embeds, code_predictor_codes = self.talker_mtp(req_input_ids, req_embeds, last_talker_hidden, text_step)
|
||||
# update the inputs_embeds and code_predictor_codes
|
||||
code_predictor_codes_cpu = code_predictor_codes.detach().to("cpu").contiguous()
|
||||
out_key = getattr(self.model, "talker_mtp_output_key", "code_predictor_codes")
|
||||
for idx, req_id in enumerate(decode_req_ids):
|
||||
req_index = self.input_batch.req_ids.index(req_id)
|
||||
start_offset = int(self.query_start_loc.cpu[req_index])
|
||||
inputs_embeds[start_offset : start_offset + 1] = req_embeds[idx : idx + 1]
|
||||
update_dict = {"code_predictor_codes": code_predictor_codes_cpu[idx : idx + 1]}
|
||||
update_dict = {out_key: code_predictor_codes_cpu[idx : idx + 1]}
|
||||
self._merge_additional_information_update(req_id, update_dict)
|
||||
|
||||
def _model_forward(
|
||||
@@ -1142,7 +1171,9 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
self._omni_last_model_output = model_output
|
||||
return model_output
|
||||
|
||||
def _merge_additional_information_update(self, req_id: str, upd: dict) -> None:
|
||||
def _merge_additional_information_update(self, req_id: str, upd: dict | None) -> None:
|
||||
if not isinstance(upd, dict):
|
||||
return
|
||||
req_state = self.requests.get(req_id)
|
||||
if req_state is None:
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user