[Core] customize process between stages, remove the need of "request_ids" in model forward, rebase to 0.12.0 (#458)
Signed-off-by: yinpeiqi <yinpeiqi809@gmail.com>
This commit is contained in:
1 parent
341457a121
commit
e9e6b3711c
6 files changed
+466
-606
No files matched your search
@@ -0,0 +1,44 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class CustomProcessMixin:
|
||||
"""
|
||||
Mixin class for all stages in the Omni model.
|
||||
"""
|
||||
|
||||
def set_custom_preprocess(self, preprocess_fn: Callable) -> None:
|
||||
"""
|
||||
Set a preprocess function for the stage.
|
||||
Args:
|
||||
preprocess_fn: The preprocess function to register.
|
||||
"""
|
||||
self.preprocess = preprocess_fn
|
||||
|
||||
def set_custom_postprocess(self, postprocess_fn: Callable) -> None:
|
||||
"""
|
||||
Set a postprocess function for the stage.
|
||||
Args:
|
||||
postprocess_fn: The postprocess function to register.
|
||||
"""
|
||||
self.postprocess = postprocess_fn
|
||||
|
||||
def preprocess(
|
||||
self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **input_dict: object
|
||||
) -> tuple[torch.Tensor, torch.Tensor, dict]:
|
||||
"""
|
||||
Process the input_ids and input_embeds for the given input_dict.
|
||||
Returns the processed input_ids, input_embeds, and the input_dict.
|
||||
If the stage don't applicable, return the original input_ids, input_embeds, and an empty dict.
|
||||
"""
|
||||
raise NotImplementedError("Preprocess is not implemented for this stage.")
|
||||
|
||||
def postprocess(self, model_output, **info_dict: object):
|
||||
"""
|
||||
Postprocess the model output.
|
||||
Returns the postprocessed model output and the save dictionary.
|
||||
Args:
|
||||
model_output: The model output to postprocess.
|
||||
"""
|
||||
raise NotImplementedError("Postprocess is not implemented for this stage.")
|
||||
@@ -30,6 +30,7 @@ from vllm.v1.outputs import SamplerOutput
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights, split_list_into_ranges
|
||||
@@ -49,10 +50,11 @@ logger = init_logger(__name__)
|
||||
dummy_inputs=Qwen2_5OmniThinkerDummyInputsBuilder,
|
||||
)
|
||||
class Qwen2_5OmniForConditionalGeneration(
|
||||
nn.Module, SupportsMultiModal, SupportsPP, Qwen2_5OmniConditionalGenerationMixin, SupportsMRoPE
|
||||
nn.Module, SupportsMultiModal, SupportsPP, SupportsMRoPE, Qwen2_5OmniConditionalGenerationMixin, CustomProcessMixin
|
||||
):
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.has_preprocess = False
|
||||
self.have_multimodal_outputs = True
|
||||
config: Qwen2_5OmniConfig = vllm_config.model_config.hf_config
|
||||
multimodal_config = vllm_config.model_config.multimodal_config
|
||||
@@ -83,6 +85,9 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
self.token2wav = None
|
||||
|
||||
elif self.model_stage == "talker":
|
||||
# register the process function for the talker stage
|
||||
self.has_preprocess = True
|
||||
self.set_custom_preprocess(self.talker_preprocess)
|
||||
self.thinker = None
|
||||
# Initialize talker model wrapper (handles projection + LM)
|
||||
self.talker = init_vllm_registered_model(
|
||||
@@ -267,151 +272,14 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
|
||||
# 2) Talker (if codec not provided)
|
||||
if self.model_stage == "talker":
|
||||
# Mixed-mode support: In a single step, both Prefill*n and Decode*n are supported.
|
||||
# Rules:
|
||||
# - Prefill segments are wrapped with special tokens: [BOS][PAD...][EOS]
|
||||
# - Decode segments consist of a single non-special token.
|
||||
# - If additional_information is provided
|
||||
# (can be a list split by request or a concatenated tensor plus a list of shapes),
|
||||
# then for each request, reconstruct the thinker→talker input embeddings for the Prefill segments;
|
||||
# - For Decode segments, if per-request auxiliary decode embeddings are provided (optional), add them;
|
||||
# otherwise, keep the original embedding.
|
||||
|
||||
if input_ids is None and additional_information is None:
|
||||
input_ids = torch.zeros(
|
||||
inputs_embeds.shape[0],
|
||||
dtype=torch.long,
|
||||
device=inputs_embeds.device,
|
||||
)
|
||||
additional_information = {}
|
||||
# mock data for profile
|
||||
if input_ids is None:
|
||||
input_ids = torch.zeros(inputs_embeds.shape[0], dtype=torch.long, device=inputs_embeds.device)
|
||||
self.thinker_reply_part = torch.zeros_like(inputs_embeds)
|
||||
is_profile = True
|
||||
else:
|
||||
is_profile = False
|
||||
|
||||
# Ensure we have base embeddings when only ids are provided
|
||||
if inputs_embeds is None and input_ids is not None:
|
||||
inputs_embeds = self.talker.embed_input_ids(input_ids)
|
||||
|
||||
# ------- Request-scoped additional information (no cross-request concat) -------
|
||||
request_ids: list[str] | None = kwargs.get("request_ids") # ordered
|
||||
request_token_spans: list[tuple[int, int]] | None = kwargs.get("request_token_spans")
|
||||
addi_by_req: dict | None = kwargs.get("additional_information_by_req_id")
|
||||
runtime_addi = kwargs.get("runtime_additional_information")
|
||||
|
||||
# Normalize runtime_addi into a mapping by request_id for convenience
|
||||
runtime_addi_by_req: dict[str, dict] = {}
|
||||
if (
|
||||
isinstance(request_ids, list)
|
||||
and isinstance(runtime_addi, list)
|
||||
and len(runtime_addi) == len(request_ids)
|
||||
):
|
||||
for i, rid in enumerate(request_ids):
|
||||
if isinstance(rid, str) and isinstance(runtime_addi[i], dict):
|
||||
runtime_addi_by_req[rid] = runtime_addi[i]
|
||||
elif isinstance(request_ids, list) and isinstance(runtime_addi, dict):
|
||||
for rid in request_ids:
|
||||
if isinstance(rid, str) and isinstance(runtime_addi.get(rid), dict):
|
||||
runtime_addi_by_req[rid] = runtime_addi[rid]
|
||||
|
||||
# Containers to return per-request updates (e.g., thinker_reply_part_per_request)
|
||||
update_by_req_id: dict[str, dict] = {}
|
||||
|
||||
# ------- Prefill: span_len > 1 -------
|
||||
if (
|
||||
not is_profile
|
||||
and isinstance(request_ids, list)
|
||||
and isinstance(request_token_spans, list)
|
||||
and isinstance(addi_by_req, dict)
|
||||
):
|
||||
for idx_req, rid in enumerate(request_ids):
|
||||
s, e = request_token_spans[idx_req]
|
||||
span_len = int(e) - int(s)
|
||||
if span_len <= 1:
|
||||
continue
|
||||
info = addi_by_req.get(rid, {}) if isinstance(rid, str) else {}
|
||||
if not isinstance(info, dict):
|
||||
info = {}
|
||||
pe = info.get("prompt_embeds") # Tensor [P,H]
|
||||
tr = info.get("thinker_result") # Tensor [K,H]
|
||||
ptoks = info.get("prompt_token_ids") # list[int]
|
||||
otoks = info.get("thinker_output_token_ids") # list[int]
|
||||
|
||||
if not isinstance(pe, torch.Tensor):
|
||||
pe = torch.zeros(
|
||||
0,
|
||||
self.talker.config.hidden_size,
|
||||
dtype=inputs_embeds.dtype,
|
||||
device=self._module_device(self.model),
|
||||
)
|
||||
if not isinstance(tr, torch.Tensor):
|
||||
tr = torch.zeros(
|
||||
0,
|
||||
self.talker.config.hidden_size,
|
||||
dtype=inputs_embeds.dtype,
|
||||
device=self._module_device(self.model),
|
||||
)
|
||||
if not isinstance(ptoks, (list, torch.Tensor)):
|
||||
ptoks = []
|
||||
if not isinstance(otoks, (list, torch.Tensor)):
|
||||
otoks = []
|
||||
|
||||
req_input_ids, req_embeds = self._thinker_to_talker_prefill(
|
||||
voice_type=voice_type,
|
||||
output_prompt_embeds=tr.to(inputs_embeds.dtype).to(self._module_device(self.model)),
|
||||
output_token_ids=otoks,
|
||||
thinker_prompt_embeds=pe.to(inputs_embeds.dtype).to(self._module_device(self.model)),
|
||||
prompt_token_ids=ptoks,
|
||||
)
|
||||
seg_len = min(span_len, req_embeds.shape[0])
|
||||
inputs_embeds[s : s + seg_len] = req_embeds[:seg_len]
|
||||
if isinstance(req_input_ids, torch.Tensor) and req_input_ids.numel() == seg_len:
|
||||
input_ids[s : s + seg_len] = req_input_ids
|
||||
|
||||
# Prepare per-request reply queue for subsequent decode: drop first row
|
||||
if tr.ndim == 2 and tr.shape[0] > 0:
|
||||
update_by_req_id.setdefault(rid, {})["thinker_reply_part_per_request"] = (
|
||||
tr[1:].detach().to("cpu").contiguous()
|
||||
)
|
||||
|
||||
# ------- Decode: span_len == 1 -------
|
||||
if not is_profile and isinstance(request_ids, list) and isinstance(request_token_spans, list):
|
||||
for idx_req, rid in enumerate(request_ids):
|
||||
s, e = request_token_spans[idx_req]
|
||||
if (int(e) - int(s)) != 1:
|
||||
continue
|
||||
# choose step vector in priority order
|
||||
step_vec = None
|
||||
# A) runtime queue
|
||||
q = None
|
||||
if isinstance(rid, str):
|
||||
q = runtime_addi_by_req.get(rid, {}).get("thinker_reply_part_per_request")
|
||||
if isinstance(q, torch.Tensor) and q.numel() > 0:
|
||||
step_vec = q[0:1]
|
||||
new_q = q[1:].detach().to("cpu").contiguous()
|
||||
update_by_req_id.setdefault(rid, {})["thinker_reply_part_per_request"] = new_q
|
||||
else:
|
||||
# B) per-request provided decode vector (optional)
|
||||
info = addi_by_req.get(rid, {}) if isinstance(addi_by_req, dict) else {}
|
||||
dv = info.get("decode_output_prompt_embeds") if isinstance(info, dict) else None
|
||||
if isinstance(dv, torch.Tensor) and dv.numel() > 0:
|
||||
step_vec = dv[0:1] if dv.ndim == 2 else dv.view(1, -1)
|
||||
elif (
|
||||
hasattr(self, "thinker_reply_part")
|
||||
and isinstance(self.thinker_reply_part, torch.Tensor)
|
||||
and self.thinker_reply_part.numel() > 0
|
||||
):
|
||||
# C) fallback shared pool
|
||||
step_vec = self.thinker_reply_part[0:1]
|
||||
self.thinker_reply_part = self.thinker_reply_part[1:]
|
||||
|
||||
if isinstance(step_vec, torch.Tensor) and step_vec.numel() > 0:
|
||||
one_id = input_ids[s : s + 1]
|
||||
_, one_embed = self._thinker_to_talker_decode_one_step(
|
||||
output_prompt_embeds=step_vec.to(inputs_embeds.dtype).to(self._module_device(self.model)),
|
||||
output_token_ids=one_id,
|
||||
)
|
||||
inputs_embeds[s] = one_embed[0]
|
||||
# TODO(Peiqi): temporal hack here to support voice_type.
|
||||
if not hasattr(self, "voice_type"):
|
||||
self.voice_type = voice_type
|
||||
|
||||
with torch.inference_mode():
|
||||
talker_hidden = self.talker(
|
||||
@@ -419,10 +287,6 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
positions=positions[0],
|
||||
inputs_embeds=inputs_embeds,
|
||||
)
|
||||
multimodal_outputs: dict = None
|
||||
# Return updates if any
|
||||
if update_by_req_id:
|
||||
multimodal_outputs = {"additional_information_update_by_req_id": update_by_req_id}
|
||||
|
||||
if sampling_metadata is not None:
|
||||
# the padding token id is set to text model's pad token id,
|
||||
@@ -431,7 +295,7 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=talker_hidden,
|
||||
multimodal_outputs=multimodal_outputs,
|
||||
multimodal_outputs=None,
|
||||
)
|
||||
|
||||
if self.model_stage == "code2wav":
|
||||
@@ -743,6 +607,74 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
return talker_hf_config.tts_text_start_token_id
|
||||
return self.tts_text_spk_token_ids[voice_type]
|
||||
|
||||
def talker_preprocess(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
input_embeds: torch.Tensor,
|
||||
**info_dict: object,
|
||||
):
|
||||
# Mixed-mode support: In a single step, both Prefill*n and Decode*n are supported.
|
||||
# Rules:
|
||||
# - Prefill segments are wrapped with special tokens: [BOS][PAD...][EOS]
|
||||
# - Decode segments consist of a single non-special token.
|
||||
# - If additional_information is provided (can be a list split by request or a
|
||||
# concatenated tensor plus a list of shapes), then for each request, reconstruct
|
||||
# the thinker→talker input embeddings for the Prefill segments;
|
||||
# - For Decode segments, if per-request auxiliary decode embeddings are provided (optional),
|
||||
# add them; otherwise, keep the original embedding.
|
||||
|
||||
# Ensure we have base embeddings when only ids are provided
|
||||
if input_embeds is None and input_ids is not None:
|
||||
input_embeds = self.talker.embed_input_ids(input_ids)
|
||||
|
||||
span_len = input_ids.shape[0]
|
||||
if span_len > 1:
|
||||
# prefill
|
||||
return self.thinker_to_talker_process(input_ids, input_embeds, **info_dict)
|
||||
else:
|
||||
# decode
|
||||
return self.thinker_to_talker_decode_one_step(input_ids, input_embeds, **info_dict)
|
||||
|
||||
def thinker_to_talker_process(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
input_embeds: torch.Tensor,
|
||||
**info_dict: object,
|
||||
):
|
||||
update_dict = {}
|
||||
|
||||
prompt_embeds = info_dict.get("prompt_embeds") # Tensor [P,H]
|
||||
thinker_result = info_dict.get("thinker_result") # Tensor [K,H]
|
||||
prompt_token_ids = info_dict.get("prompt_token_ids") # list[int]
|
||||
thinker_output_token_ids = info_dict.get("thinker_output_token_ids") # list[int]
|
||||
|
||||
if not isinstance(prompt_embeds, torch.Tensor):
|
||||
prompt_embeds = torch.zeros(
|
||||
0, self.talker.config.hidden_size, dtype=input_embeds.dtype, device=self._module_device(self.model)
|
||||
)
|
||||
if not isinstance(thinker_result, torch.Tensor):
|
||||
thinker_result = torch.zeros(
|
||||
0, self.talker.config.hidden_size, dtype=input_embeds.dtype, device=self._module_device(self.model)
|
||||
)
|
||||
if not isinstance(prompt_token_ids, (list, torch.Tensor)):
|
||||
prompt_token_ids = []
|
||||
if not isinstance(thinker_output_token_ids, (list, torch.Tensor)):
|
||||
thinker_output_token_ids = []
|
||||
|
||||
# TODO(Peiqi): add voice_type support
|
||||
req_input_ids, req_embeds = self._thinker_to_talker_prefill(
|
||||
voice_type=self.voice_type,
|
||||
output_prompt_embeds=thinker_result.to(input_embeds.dtype).to(self._module_device(self.model)),
|
||||
output_token_ids=thinker_output_token_ids,
|
||||
thinker_prompt_embeds=prompt_embeds.to(input_embeds.dtype).to(self._module_device(self.model)),
|
||||
prompt_token_ids=prompt_token_ids,
|
||||
)
|
||||
|
||||
if thinker_result.ndim == 2 and thinker_result.shape[0] > 0:
|
||||
update_dict["thinker_reply_part"] = thinker_result[1:].detach().to("cpu").contiguous()
|
||||
|
||||
return req_input_ids, req_embeds, update_dict
|
||||
|
||||
def _thinker_to_talker_prefill(
|
||||
self,
|
||||
voice_type: str,
|
||||
@@ -788,6 +720,38 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
)
|
||||
return prompt_token_ids_processed, prompt_embeds
|
||||
|
||||
def thinker_to_talker_decode_one_step(self, input_ids, input_embeds, **info_dict):
|
||||
update_dict = {}
|
||||
# choose step vector in priority order
|
||||
step_vec = None
|
||||
q = info_dict.get("thinker_reply_part", None)
|
||||
if isinstance(q, torch.Tensor) and q.numel() > 0:
|
||||
step_vec = q[0:1]
|
||||
new_q = q[1:].detach().to("cpu").contiguous()
|
||||
update_dict["thinker_reply_part"] = new_q
|
||||
else:
|
||||
# B) per-request provided decode vector (optional)
|
||||
dv = info_dict.get("decode_output_prompt_embeds") if isinstance(info_dict, dict) else None
|
||||
if isinstance(dv, torch.Tensor) and dv.numel() > 0:
|
||||
step_vec = dv[0:1] if dv.ndim == 2 else dv.view(1, -1)
|
||||
elif (
|
||||
hasattr(self, "thinker_reply_part")
|
||||
and isinstance(self.thinker_reply_part, torch.Tensor)
|
||||
and self.thinker_reply_part.numel() > 0
|
||||
):
|
||||
# C) fallback shared pool
|
||||
step_vec = self.thinker_reply_part[0:1]
|
||||
self.thinker_reply_part = self.thinker_reply_part[1:]
|
||||
|
||||
if isinstance(step_vec, torch.Tensor) and step_vec.numel() > 0:
|
||||
one_id = input_ids[0:1]
|
||||
_, one_embed = self._thinker_to_talker_decode_one_step(
|
||||
output_prompt_embeds=step_vec.to(input_embeds.dtype).to(self._module_device(self.model)),
|
||||
output_token_ids=one_id,
|
||||
)
|
||||
input_embeds[0] = one_embed[0]
|
||||
return input_ids[0:1], input_embeds[0:1], update_dict
|
||||
|
||||
def _thinker_to_talker_decode_one_step(
|
||||
self,
|
||||
output_prompt_embeds,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
# Copyright 2025 The Qwen team.
|
||||
"""Inference-only Qwen3-Omni-Moe unified model (thinker + talker + code2wav)."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable
|
||||
from functools import cached_property
|
||||
|
||||
@@ -31,8 +30,9 @@ from vllm.v1.outputs import SamplerOutput
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights
|
||||
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights, safe_tensor_reshape
|
||||
|
||||
# Special token IDs for Qwen3 Omni MoE
|
||||
# Reference: https://huggingface.co/Qwen/Qwen3-Omni-30B-A3B-Instruct/blob/main/tokenizer_config.json
|
||||
@@ -65,7 +65,7 @@ logger = init_logger(__name__)
|
||||
dummy_inputs=Qwen3OmniMoeThinkerDummyInputsBuilder,
|
||||
)
|
||||
class Qwen3OmniMoeForConditionalGeneration(
|
||||
nn.Module, SupportsMultiModal, SupportsPP, Qwen3OmniMoeConditionalGenerationMixin
|
||||
nn.Module, SupportsMultiModal, SupportsPP, Qwen3OmniMoeConditionalGenerationMixin, CustomProcessMixin
|
||||
):
|
||||
"""
|
||||
Unified Qwen3 Omni MoE model combining thinker, talker, and code2wav.
|
||||
@@ -82,6 +82,8 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.have_multimodal_outputs = True
|
||||
self.has_preprocess = False
|
||||
self.has_postprocess = False
|
||||
config: Qwen3OmniMoeConfig = vllm_config.model_config.hf_config
|
||||
multimodal_config = vllm_config.model_config.multimodal_config
|
||||
|
||||
@@ -121,6 +123,10 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
self.talker = None
|
||||
self.code2wav = None
|
||||
elif self.model_stage == "talker":
|
||||
self.has_preprocess = True
|
||||
self.has_postprocess = True
|
||||
self.set_custom_preprocess(self.talker_preprocess)
|
||||
self.set_custom_postprocess(self.talker_postprocess)
|
||||
self.thinker = None
|
||||
# Initialize talker model (text embeddings → codec codes)
|
||||
# Create a new vllm_config with talker_config as the hf_config
|
||||
@@ -359,19 +365,8 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
|
||||
# ========== Stage 2.1: Talker ==========
|
||||
elif self.model_stage == "talker":
|
||||
# Mixed-mode support: In a single step, both Prefill*n and Decode*n are supported.
|
||||
# Rules:
|
||||
# - Prefill segments are wrapped with special tokens: [BOS][PAD...][EOS]
|
||||
# - Decode segments consist of a single non-special token.
|
||||
# - If additional_information is provided (can be a list split by request
|
||||
# or a concatenated tensor plus a list of shapes), then for each request,
|
||||
# reconstruct the thinker→talker input embeddings for the Prefill segments;
|
||||
# - For Decode segments, if per-request auxiliary decode embeddings are
|
||||
# provided (optional), add them; otherwise, keep the original embedding.
|
||||
|
||||
if input_ids is None and additional_information is None:
|
||||
if input_ids is None:
|
||||
input_ids = torch.zeros(inputs_embeds.shape[0], dtype=torch.long, device=inputs_embeds.device)
|
||||
additional_information = {}
|
||||
self.code_predictor_hidden_pool = torch.zeros_like(inputs_embeds)
|
||||
is_profile = True
|
||||
else:
|
||||
@@ -381,274 +376,39 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
if inputs_embeds is None and input_ids is not None:
|
||||
inputs_embeds = self.talker.embed_input_ids(input_ids)
|
||||
|
||||
# ------- Request-scoped additional information (no cross-request concat) -------
|
||||
request_ids: list[str] | None = kwargs.get("request_ids") # ordered
|
||||
request_token_spans: list[tuple[int, int]] | None = kwargs.get("request_token_spans")
|
||||
addi_by_req: dict | None = kwargs.get("additional_information_by_req_id")
|
||||
runtime_addi = kwargs.get("runtime_additional_information")
|
||||
|
||||
# Normalize runtime_addi into a mapping by request_id for convenience
|
||||
runtime_addi_by_req: dict[str, dict] = {}
|
||||
if (
|
||||
isinstance(request_ids, list)
|
||||
and isinstance(runtime_addi, list)
|
||||
and len(runtime_addi) == len(request_ids)
|
||||
):
|
||||
for i, rid in enumerate(request_ids):
|
||||
if isinstance(rid, str) and isinstance(runtime_addi[i], dict):
|
||||
runtime_addi_by_req[rid] = runtime_addi[i]
|
||||
elif isinstance(request_ids, list) and isinstance(runtime_addi, dict):
|
||||
for rid in request_ids:
|
||||
if isinstance(rid, str) and isinstance(runtime_addi.get(rid), dict):
|
||||
runtime_addi_by_req[rid] = runtime_addi[rid]
|
||||
|
||||
# Containers to return per-request updates (e.g., code_predictor_hidden_per_request)
|
||||
update_by_req_id: dict[str, dict] = {}
|
||||
text_step = torch.zeros(
|
||||
1,
|
||||
self.talker_config.text_config.hidden_size,
|
||||
device=self._module_device(self.talker),
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
last_talker_hidden = torch.zeros(
|
||||
1,
|
||||
1,
|
||||
self.talker_config.text_config.hidden_size,
|
||||
device=self._module_device(self.talker),
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
|
||||
batched_talker_inputs = defaultdict(dict)
|
||||
if is_profile:
|
||||
batched_talker_inputs["0"] = {
|
||||
"input_ids": input_ids,
|
||||
"positions": positions,
|
||||
"inputs_embeds": inputs_embeds.clone(),
|
||||
"text_step": text_step,
|
||||
"last_talker_hidden": last_talker_hidden,
|
||||
}
|
||||
|
||||
# ------- Prefill: span_len > 1 -------
|
||||
if (
|
||||
not is_profile
|
||||
and isinstance(request_ids, list)
|
||||
and isinstance(request_token_spans, list)
|
||||
and isinstance(addi_by_req, dict)
|
||||
):
|
||||
for idx_req, rid in enumerate(request_ids):
|
||||
s, e = request_token_spans[idx_req]
|
||||
span_len = int(e) - int(s)
|
||||
if span_len <= 1:
|
||||
continue
|
||||
batched_talker_inputs[rid] = {
|
||||
"input_ids": input_ids[s:e],
|
||||
"positions": positions[s:e] if len(positions.shape) == 1 else positions[0, s:e],
|
||||
"inputs_embeds": inputs_embeds[s:e].clone(),
|
||||
"text_step": text_step,
|
||||
"last_talker_hidden": last_talker_hidden,
|
||||
}
|
||||
info = addi_by_req.get(rid, {}) if isinstance(rid, str) else {}
|
||||
# Read thinker outputs for prefill
|
||||
thinker_sequence_embeds = info.get("thinker_embeddings").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
) # Tensor [P,H]
|
||||
thinker_hidden_states = info.get("thinker_hidden_states").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
) # Tensor [K,H]
|
||||
thinker_sequences = (
|
||||
info.get("thinker_sequences")
|
||||
if info.get("thinker_sequences") is None
|
||||
else torch.as_tensor(info.get("thinker_sequences"), device=self._module_device(self.talker))
|
||||
)
|
||||
thinker_chatml_ids = (
|
||||
info.get("thinker_input_ids")
|
||||
if info.get("thinker_input_ids") is None
|
||||
else torch.as_tensor(info.get("thinker_input_ids"), device=self._module_device(self.talker))
|
||||
)
|
||||
# Optional thinker-side TTS token embeddings (to be projected by talker)
|
||||
tts_bos_thinker = info.get("tts_bos_embed").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
)
|
||||
tts_eos_thinker = info.get("tts_eos_embed").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
)
|
||||
tts_pad_thinker = info.get("tts_pad_embed").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
)
|
||||
|
||||
if thinker_sequence_embeds is None or thinker_hidden_states is None:
|
||||
raise ValueError(
|
||||
f"additional_information_by_req_id[{rid}] must include "
|
||||
f"'thinker_embeddings' and 'thinker_hidden_states' for talker prefill."
|
||||
)
|
||||
|
||||
# Normalize to tensors
|
||||
if not isinstance(thinker_sequence_embeds, torch.Tensor):
|
||||
thinker_sequence_embeds = torch.as_tensor(
|
||||
thinker_sequence_embeds, device=self._module_device(self.talker)
|
||||
)
|
||||
if not isinstance(thinker_hidden_states, torch.Tensor):
|
||||
thinker_hidden_states = torch.as_tensor(
|
||||
thinker_hidden_states, device=self._module_device(self.talker)
|
||||
)
|
||||
|
||||
if isinstance(thinker_chatml_ids, torch.Tensor) or isinstance(thinker_chatml_ids, list):
|
||||
ids_chatml = (
|
||||
thinker_chatml_ids
|
||||
if isinstance(thinker_chatml_ids, torch.Tensor)
|
||||
else torch.as_tensor(thinker_chatml_ids, device=self._module_device(self.talker))
|
||||
)
|
||||
if ids_chatml.ndim == 1:
|
||||
ids_chatml = ids_chatml.unsqueeze(0)
|
||||
else:
|
||||
# Fallback: create dummy ids if not provided
|
||||
ids_chatml = torch.zeros(
|
||||
(1, thinker_sequence_embeds.shape[1]),
|
||||
dtype=torch.long,
|
||||
device=self._module_device(self.talker),
|
||||
)
|
||||
thinker_sequences = ids_chatml
|
||||
|
||||
speaker_id = self._get_text_spk_token_id(voice_type)
|
||||
req_input_ids, req_embeds, trailing_text_hidden = self._thinker_to_talker_prefill(
|
||||
thinker_embed=thinker_sequence_embeds.to(self._module_device(self.talker)),
|
||||
thinker_hidden=thinker_hidden_states.to(self._module_device(self.talker)),
|
||||
multimodal_mask=None,
|
||||
input_ids=ids_chatml.to(self._module_device(self.talker)),
|
||||
thinker_result_ids=thinker_sequences.to(self._module_device(self.talker)),
|
||||
speaker_id=speaker_id,
|
||||
tts_bos_thinker=tts_bos_thinker,
|
||||
tts_eos_thinker=tts_eos_thinker,
|
||||
tts_pad_thinker=tts_pad_thinker,
|
||||
)
|
||||
# req_embeds: [S,D], req_input_ids: [S] → map onto the current span
|
||||
seg_len = min(span_len, req_embeds.shape[0])
|
||||
# Force-align dtype/device/memory layout to avoid mismatches from precision or layout differences
|
||||
inputs_embeds[s : s + seg_len] = (
|
||||
req_embeds[:seg_len].to(device=inputs_embeds.device, dtype=inputs_embeds.dtype).contiguous()
|
||||
)
|
||||
|
||||
if isinstance(req_input_ids, torch.Tensor) and req_input_ids.shape[0] >= seg_len:
|
||||
input_ids[s : s + seg_len] = req_input_ids[:seg_len].to(input_ids.device)
|
||||
# Queue tailing_text_hidden for decode (drop first for next steps),
|
||||
# similar to Qwen2.5's thinker_reply_part_per_request
|
||||
batched_talker_inputs[rid]["inputs_embeds"] = inputs_embeds[s : s + seg_len].clone()
|
||||
try:
|
||||
if isinstance(trailing_text_hidden, torch.Tensor) and trailing_text_hidden.numel() > 0:
|
||||
if trailing_text_hidden.ndim == 2:
|
||||
rem_tail = trailing_text_hidden
|
||||
elif trailing_text_hidden.ndim == 1:
|
||||
rem_tail = torch.zeros(
|
||||
0,
|
||||
trailing_text_hidden.shape[0],
|
||||
dtype=trailing_text_hidden.dtype,
|
||||
device=trailing_text_hidden.device,
|
||||
)
|
||||
else:
|
||||
# compatible with old shape [1,S,D]
|
||||
rem_tail = trailing_text_hidden.squeeze(0)
|
||||
if rem_tail.shape[0] > 0:
|
||||
update_by_req_id.setdefault(rid, {})["tailing_text_hidden_per_request"] = (
|
||||
rem_tail.detach().to("cpu").contiguous()
|
||||
)
|
||||
# Also persist projected tts_pad for decode fallback if needed
|
||||
if isinstance(tts_pad_thinker, torch.Tensor):
|
||||
pad_in = tts_pad_thinker
|
||||
if pad_in.ndim == 2:
|
||||
pad_in = pad_in.unsqueeze(0)
|
||||
if pad_in.ndim == 1:
|
||||
pad_in = pad_in.view(1, 1, -1)
|
||||
pad_proj = self.talker.text_projection(pad_in.to(self._module_device(self.talker)))
|
||||
update_by_req_id.setdefault(rid, {})["tts_pad_embed_projected"] = (
|
||||
pad_proj.detach().to("cpu").contiguous()
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------- Decode: span_len == 1 -------
|
||||
if not is_profile and isinstance(request_ids, list) and isinstance(request_token_spans, list):
|
||||
for idx_req, rid in enumerate(request_ids):
|
||||
s, e = request_token_spans[idx_req]
|
||||
if (int(e) - int(s)) != 1:
|
||||
continue
|
||||
batched_talker_inputs[rid] = {
|
||||
"input_ids": input_ids[s:e],
|
||||
"positions": positions[s:e] if len(positions.shape) == 1 else positions[0, s:e],
|
||||
"inputs_embeds": inputs_embeds[s:e].clone(),
|
||||
"text_step": text_step,
|
||||
"last_talker_hidden": last_talker_hidden,
|
||||
}
|
||||
try:
|
||||
q_tail = None
|
||||
if isinstance(rid, str):
|
||||
q_tail = runtime_addi_by_req.get(rid, {}).get("tailing_text_hidden_per_request")
|
||||
if isinstance(q_tail, torch.Tensor) and q_tail.numel() > 0:
|
||||
use_vec = q_tail[0:1, :]
|
||||
new_q_tail = (
|
||||
q_tail[1:, :].detach().to("cpu").contiguous()
|
||||
if q_tail.shape[1] > 1
|
||||
else self.tts_pad_embed.to(inputs_embeds.device, dtype=inputs_embeds.dtype)
|
||||
)
|
||||
text_step = use_vec.to(inputs_embeds.device, dtype=inputs_embeds.dtype)
|
||||
update_by_req_id.setdefault(rid, {})["tailing_text_hidden_per_request"] = new_q_tail
|
||||
else:
|
||||
text_step = self.tts_pad_embed.to(inputs_embeds.device, dtype=inputs_embeds.dtype)
|
||||
|
||||
if isinstance(rid, str):
|
||||
last_talker_hidden = (
|
||||
runtime_addi_by_req.get(rid, {})
|
||||
.get("last_talker_hidden")
|
||||
.to(inputs_embeds.device, dtype=inputs_embeds.dtype)
|
||||
)
|
||||
last_talker_hidden = last_talker_hidden.reshape(
|
||||
*last_talker_hidden.shape[-2:]
|
||||
) # [1, hidden_size]
|
||||
batched_talker_inputs[rid]["last_talker_hidden"] = last_talker_hidden
|
||||
batched_talker_inputs[rid]["text_step"] = text_step
|
||||
except Exception as e:
|
||||
logger.error(f"Error in decode: {e}")
|
||||
|
||||
if not is_profile and input_ids.shape[0] > 1:
|
||||
self.input_ids_list = input_ids.tolist()
|
||||
|
||||
last_talker_hidden = None
|
||||
# TODO(Peiqi): temporal hack here to support voice_type.
|
||||
if not hasattr(self, "voice_type"):
|
||||
self.voice_type = voice_type
|
||||
|
||||
# Run talker forward
|
||||
with torch.inference_mode():
|
||||
talker_hidden, batched_code_predictor_codes = self.talker(
|
||||
batched_talker_inputs=batched_talker_inputs,
|
||||
talker_hidden = self.talker.forward(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
inputs_embeds=inputs_embeds,
|
||||
)
|
||||
if not is_profile:
|
||||
for rid in request_ids:
|
||||
idx_req = request_ids.index(rid)
|
||||
s, e = request_token_spans[idx_req]
|
||||
span_len = int(e) - int(s)
|
||||
update_by_req_id.setdefault(rid, {})["last_talker_hidden"] = (
|
||||
talker_hidden[e - 1 : e, :].detach().to("cpu").contiguous()
|
||||
)
|
||||
|
||||
# merge the code_predictor_codes from the info_dict list into a single tensor
|
||||
multimodal_outputs: dict = None
|
||||
# Return updates if any
|
||||
if update_by_req_id:
|
||||
multimodal_outputs = {"additional_information_update_by_req_id": update_by_req_id}
|
||||
if not is_profile and multimodal_outputs is not None and batched_code_predictor_codes is not None:
|
||||
multimodal_outputs.update({"code_predictor_codes": batched_code_predictor_codes})
|
||||
elif not is_profile and multimodal_outputs is None and batched_code_predictor_codes is not None:
|
||||
multimodal_outputs = {"code_predictor_codes": batched_code_predictor_codes}
|
||||
if not is_profile:
|
||||
# Here is the only place to use runtime_additional_information. After MTP in the
|
||||
# preprocess function, the code_predictor_codes are stored in the info_dict list.
|
||||
# We need to merge the tensors from different requests into a single tensor.
|
||||
# In the future, we may allow user to custom an aggregated function.
|
||||
info_dicts = kwargs.get("runtime_additional_information")
|
||||
code_predictor_codes = [info.get("code_predictor_codes") for info in info_dicts]
|
||||
multimodal_outputs = {"code_predictor_codes": torch.cat(code_predictor_codes, dim=0)}
|
||||
|
||||
next_token_id = None
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=talker_hidden,
|
||||
multimodal_outputs=multimodal_outputs,
|
||||
next_token_id=next_token_id,
|
||||
)
|
||||
return OmniOutput(text_hidden_states=talker_hidden, multimodal_outputs=multimodal_outputs)
|
||||
|
||||
# ========== Stage 3: Code2Wav ==========
|
||||
elif self.model_stage == "code2wav":
|
||||
# Extract codec codes from input
|
||||
codes = []
|
||||
if input_ids is not None:
|
||||
# addi_by_req = kwargs.get("additional_information_by_req_id")
|
||||
codes.append(input_ids.reshape(1, 16, -1))
|
||||
|
||||
else:
|
||||
@@ -801,6 +561,183 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
return self.tts_text_spk_token_ids[self.default_tts_text_spk_type]
|
||||
return self.tts_text_spk_token_ids[voice_type]
|
||||
|
||||
def talker_postprocess(self, hidden_states: torch.Tensor, **info_dict: object):
|
||||
"""
|
||||
Postprocess the talker hidden states.
|
||||
"""
|
||||
update_dict = {}
|
||||
update_dict["last_talker_hidden"] = hidden_states[-1, :].detach().to("cpu").contiguous()
|
||||
return update_dict
|
||||
|
||||
def talker_preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
|
||||
"""
|
||||
Preprocess talker embeds. Noted that we set the MTP here.
|
||||
"""
|
||||
# Ensure we have base embeddings when only ids are provided
|
||||
if input_embeds is None and input_ids is not None:
|
||||
input_embeds = self.talker.embed_input_ids(input_ids)
|
||||
|
||||
text_step = torch.zeros(
|
||||
1,
|
||||
self.talker_config.text_config.hidden_size,
|
||||
device=self._module_device(self.talker),
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
last_talker_hidden = torch.zeros(
|
||||
1,
|
||||
1,
|
||||
self.talker_config.text_config.hidden_size,
|
||||
device=self._module_device(self.talker),
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
|
||||
span_len = input_ids.shape[0]
|
||||
if span_len > 1:
|
||||
# prefill
|
||||
input_ids, input_embeds, update_dict = self.talker_preprocess_prefill(input_ids, input_embeds, **info_dict)
|
||||
else:
|
||||
last_talker_hidden, text_step, update_dict = self.talker_preprocess_decode(
|
||||
input_ids, input_embeds, **info_dict
|
||||
)
|
||||
|
||||
# execute talker MTP
|
||||
input_embeds, code_predictor_codes = self.talker_mtp(input_ids, input_embeds, last_talker_hidden, text_step)
|
||||
update_dict["code_predictor_codes"] = code_predictor_codes
|
||||
|
||||
return input_ids, input_embeds, update_dict
|
||||
|
||||
def talker_mtp(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
input_embeds: torch.Tensor,
|
||||
last_talker_hidden: torch.Tensor,
|
||||
text_step: torch.Tensor,
|
||||
):
|
||||
# TODO(Peiqi): not support intermediate_tensors now
|
||||
input_ids = safe_tensor_reshape(input_ids, (1, -1))
|
||||
inputs_embeds = safe_tensor_reshape(input_embeds, (-1, self.talker_config.text_config.hidden_size))
|
||||
text_step = safe_tensor_reshape(text_step, (1, -1))
|
||||
last_talker_hidden = safe_tensor_reshape(last_talker_hidden, (1, 1, self.talker_config.text_config.hidden_size))
|
||||
# for profiling
|
||||
if inputs_embeds.shape[-1] == 2048:
|
||||
inputs_embeds = self.text_projection(inputs_embeds)
|
||||
if inputs_embeds.shape[0] == 1:
|
||||
code_predictor_codes, summed_embeddings = self.talker.code_predictor_forward(
|
||||
input_ids, inputs_embeds.clone(), last_talker_hidden=last_talker_hidden
|
||||
)
|
||||
inputs_embeds = summed_embeddings.clone()
|
||||
else:
|
||||
code_predictor_codes = torch.zeros((0, self.talker.num_code_groups), dtype=torch.long)
|
||||
inputs_embeds = (inputs_embeds + text_step).reshape(-1, self.talker_config.text_config.hidden_size)
|
||||
return inputs_embeds, code_predictor_codes.squeeze(-1).detach().to("cpu").contiguous()
|
||||
|
||||
def talker_preprocess_prefill(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
|
||||
# Containers to return per-request updates (e.g., code_predictor_hidden_per_request)
|
||||
update_dict: dict[str, dict] = {}
|
||||
# TODO(Peiqi): add voice_type support
|
||||
voice_type = self.voice_type
|
||||
|
||||
# Read thinker outputs for prefill
|
||||
thinker_sequence_embeds = info_dict.get("thinker_embeddings").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
) # Tensor [P,H]
|
||||
thinker_hidden_states = info_dict.get("thinker_hidden_states").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
) # Tensor [K,H]
|
||||
thinker_sequences = (
|
||||
info_dict.get("thinker_sequences")
|
||||
if info_dict.get("thinker_sequences") is None
|
||||
else torch.as_tensor(info_dict.get("thinker_sequences"), device=self._module_device(self.talker))
|
||||
)
|
||||
thinker_chatml_ids = (
|
||||
info_dict.get("thinker_input_ids")
|
||||
if info_dict.get("thinker_input_ids") is None
|
||||
else torch.as_tensor(info_dict.get("thinker_input_ids"), device=self._module_device(self.talker))
|
||||
)
|
||||
# Optional thinker-side TTS token embeddings (to be projected by talker)
|
||||
tts_bos_thinker = info_dict.get("tts_bos_embed").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
)
|
||||
tts_eos_thinker = info_dict.get("tts_eos_embed").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
)
|
||||
tts_pad_thinker = info_dict.get("tts_pad_embed").to(
|
||||
device=self._module_device(self.talker), dtype=torch.bfloat16
|
||||
)
|
||||
|
||||
if thinker_sequence_embeds is None or thinker_hidden_states is None:
|
||||
raise ValueError(
|
||||
"additional_information_by_req_id must include "
|
||||
"'thinker_embeddings' and 'thinker_hidden_states' for talker prefill."
|
||||
)
|
||||
|
||||
# Normalize to tensors
|
||||
if not isinstance(thinker_sequence_embeds, torch.Tensor):
|
||||
thinker_sequence_embeds = torch.as_tensor(thinker_sequence_embeds, device=self._module_device(self.talker))
|
||||
if not isinstance(thinker_hidden_states, torch.Tensor):
|
||||
thinker_hidden_states = torch.as_tensor(thinker_hidden_states, device=self._module_device(self.talker))
|
||||
|
||||
if isinstance(thinker_chatml_ids, torch.Tensor) or isinstance(thinker_chatml_ids, list):
|
||||
ids_chatml = (
|
||||
thinker_chatml_ids
|
||||
if isinstance(thinker_chatml_ids, torch.Tensor)
|
||||
else torch.as_tensor(thinker_chatml_ids, device=self._module_device(self.talker))
|
||||
)
|
||||
if ids_chatml.ndim == 1:
|
||||
ids_chatml = ids_chatml.unsqueeze(0)
|
||||
else:
|
||||
# Fallback: create dummy ids if not provided
|
||||
ids_chatml = torch.zeros(
|
||||
(1, thinker_sequence_embeds.shape[1]),
|
||||
dtype=torch.long,
|
||||
device=self._module_device(self.talker),
|
||||
)
|
||||
thinker_sequences = ids_chatml
|
||||
|
||||
speaker_id = self._get_text_spk_token_id(voice_type)
|
||||
req_input_ids, req_embeds, trailing_text_hidden = self._thinker_to_talker_prefill(
|
||||
thinker_embed=thinker_sequence_embeds.to(self._module_device(self.talker)),
|
||||
thinker_hidden=thinker_hidden_states.to(self._module_device(self.talker)),
|
||||
multimodal_mask=None,
|
||||
input_ids=ids_chatml.to(self._module_device(self.talker)),
|
||||
thinker_result_ids=thinker_sequences.to(self._module_device(self.talker)),
|
||||
speaker_id=speaker_id,
|
||||
tts_bos_thinker=tts_bos_thinker,
|
||||
tts_eos_thinker=tts_eos_thinker,
|
||||
tts_pad_thinker=tts_pad_thinker,
|
||||
)
|
||||
|
||||
# Queue tailing_text_hidden for decode (drop first for next steps),
|
||||
try:
|
||||
if isinstance(trailing_text_hidden, torch.Tensor) and trailing_text_hidden.numel() > 0:
|
||||
if trailing_text_hidden.ndim == 2:
|
||||
rem_tail = trailing_text_hidden
|
||||
elif trailing_text_hidden.ndim == 1:
|
||||
rem_tail = torch.zeros(
|
||||
0,
|
||||
trailing_text_hidden.shape[0],
|
||||
dtype=trailing_text_hidden.dtype,
|
||||
device=trailing_text_hidden.device,
|
||||
)
|
||||
else:
|
||||
# compatible with old shape [1,S,D]
|
||||
rem_tail = trailing_text_hidden.squeeze(0)
|
||||
if rem_tail.shape[0] > 0:
|
||||
update_dict["tailing_text_hidden"] = rem_tail.detach().to("cpu").contiguous()
|
||||
# Also persist projected tts_pad for decode fallback if needed
|
||||
if isinstance(tts_pad_thinker, torch.Tensor):
|
||||
pad_in = tts_pad_thinker
|
||||
if pad_in.ndim == 2:
|
||||
pad_in = pad_in.unsqueeze(0)
|
||||
if pad_in.ndim == 1:
|
||||
pad_in = pad_in.view(1, 1, -1)
|
||||
pad_proj = self.talker.text_projection(pad_in.to(self._module_device(self.talker)))
|
||||
update_dict["tts_pad_embed_projected"] = pad_proj.detach().to("cpu").contiguous()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return req_input_ids, req_embeds, update_dict
|
||||
|
||||
def _thinker_to_talker_prefill(
|
||||
self,
|
||||
thinker_embed: torch.Tensor,
|
||||
@@ -905,6 +842,30 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
|
||||
return talker_input_id, talker_input_embed, trailing_text_hidden_all
|
||||
|
||||
def talker_preprocess_decode(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
|
||||
update_dict: dict[str, dict] = {}
|
||||
|
||||
try:
|
||||
q_tail = info_dict.get("tailing_text_hidden")
|
||||
if isinstance(q_tail, torch.Tensor) and q_tail.numel() > 0:
|
||||
use_vec = q_tail[0:1, :]
|
||||
new_q_tail = (
|
||||
q_tail[1:, :].detach().to("cpu").contiguous()
|
||||
if q_tail.shape[1] > 1
|
||||
else self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
|
||||
)
|
||||
text_step = use_vec.to(input_embeds.device, dtype=input_embeds.dtype)
|
||||
update_dict["tailing_text_hidden"] = new_q_tail
|
||||
else:
|
||||
text_step = self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
|
||||
|
||||
last_talker_hidden = info_dict.get("last_talker_hidden").to(input_embeds.device, dtype=input_embeds.dtype)
|
||||
last_talker_hidden = last_talker_hidden.reshape(*last_talker_hidden.shape[-2:]) # [1, hidden_size]
|
||||
except Exception as e:
|
||||
logger.error(f"Error in decode: {e}")
|
||||
|
||||
return last_talker_hidden, text_step, update_dict
|
||||
|
||||
def _get_talker_user_parts(self, im_start_index, segment_end_index, multimodal_mask, thinker_hidden, thinker_embed):
|
||||
user_talker_part = torch.empty(
|
||||
(segment_end_index - im_start_index, self.config.talker_config.text_config.hidden_size),
|
||||
|
||||
@@ -48,7 +48,6 @@ try:
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
flash_attn = None
|
||||
|
||||
from vllm_omni.model_executor.models.utils import safe_tensor_reshape
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -368,75 +367,22 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batched_talker_inputs: dict,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
inputs_embeds: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
**kwargs: object,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
"""Forward pass through the talker model.
|
||||
|
||||
For inference, the talker receives inputs_embeds that should already be
|
||||
projected to talker's hidden dimension. If receiving raw thinker outputs,
|
||||
use project_thinker_outputs() first.
|
||||
"""
|
||||
# If intermediate_tensors is provided (pipeline parallel),
|
||||
# inputs_embeds should be None
|
||||
batched_input_ids = []
|
||||
batched_positions = []
|
||||
batched_input_embeds = []
|
||||
batched_code_predictor_codes = []
|
||||
if intermediate_tensors is not None:
|
||||
inputs_embeds = None
|
||||
for rid in batched_talker_inputs:
|
||||
input_ids = safe_tensor_reshape(batched_talker_inputs[rid]["input_ids"], (1, -1))
|
||||
positions = batched_talker_inputs[rid]["positions"]
|
||||
inputs_embeds = safe_tensor_reshape(
|
||||
batched_talker_inputs[rid]["inputs_embeds"], (-1, self.config.text_config.hidden_size)
|
||||
)
|
||||
text_step = safe_tensor_reshape(batched_talker_inputs[rid]["text_step"], (1, -1))
|
||||
last_talker_hidden = safe_tensor_reshape(
|
||||
batched_talker_inputs[rid]["last_talker_hidden"], (1, 1, self.config.text_config.hidden_size)
|
||||
)
|
||||
# for profiling
|
||||
if inputs_embeds.shape[-1] == 2048:
|
||||
inputs_embeds = self.text_projection(inputs_embeds)
|
||||
if inputs_embeds.shape[0] == 1:
|
||||
code_predictor_codes, summed_embeddings = self.code_predictor_forward(
|
||||
input_ids, inputs_embeds.clone(), last_talker_hidden=last_talker_hidden
|
||||
)
|
||||
inputs_embeds = summed_embeddings.clone()
|
||||
else:
|
||||
code_predictor_codes = torch.zeros((0, self.num_code_groups), dtype=torch.long)
|
||||
batched_input_ids.append(input_ids.reshape(-1))
|
||||
batched_positions.append(positions)
|
||||
batched_input_embeds.append((inputs_embeds + text_step).reshape(-1, self.config.text_config.hidden_size))
|
||||
batched_code_predictor_codes.append(code_predictor_codes.squeeze(-1).detach().to("cpu").contiguous())
|
||||
try:
|
||||
talker_input_ids = torch.cat(batched_input_ids, dim=0)
|
||||
talker_positions = torch.cat(batched_positions, dim=0)
|
||||
talker_input_embeds = torch.cat(batched_input_embeds, dim=0)
|
||||
except Exception as e:
|
||||
print(f"Error in talker_input_embeds: {e}")
|
||||
print(f"talker_input_embeds shape: {[tensor.shape for tensor in batched_input_embeds]}")
|
||||
print(f"talker_input_ids shape: {[tensor.shape for tensor in batched_input_ids]}")
|
||||
print(f"talker_positions shape: {[tensor.shape for tensor in batched_positions]}")
|
||||
raise e
|
||||
"""Forward pass through the talker model."""
|
||||
talker_hidden_states, _ = self.language_model.model(
|
||||
talker_input_ids,
|
||||
talker_positions,
|
||||
input_ids,
|
||||
positions,
|
||||
intermediate_tensors,
|
||||
inputs_embeds=talker_input_embeds,
|
||||
inputs_embeds=inputs_embeds,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Pass talker hidden states to code predictor
|
||||
# Returns: (residual_codes, summed_embeddings=sum of all layer embeddings at each position)
|
||||
|
||||
# Return both talker hidden states and code predictor results
|
||||
# code_predictor_codes: [batch, num_code_groups, seq_len]
|
||||
# summed_embeddings: [batch, seq_len, hidden_size]
|
||||
# - Sum of all layer embeddings at each position (like Transformers)
|
||||
|
||||
return talker_hidden_states, batched_code_predictor_codes
|
||||
return talker_hidden_states
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
|
||||
@@ -374,8 +374,6 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
with record_function_or_nullcontext("gpu_model_runner: eplb"):
|
||||
self.eplb_step()
|
||||
|
||||
self._process_additional_information_updates(multimodal_outputs)
|
||||
|
||||
hidden_states_cpu = hidden_states.detach().to("cpu").contiguous()
|
||||
num_scheduled_tokens_np = getattr(self, "_omni_num_scheduled_tokens_np", None)
|
||||
if num_scheduled_tokens_np is None:
|
||||
@@ -385,6 +383,8 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
dtype=np.int32,
|
||||
)
|
||||
|
||||
self._process_additional_information_updates(hidden_states, multimodal_outputs, num_scheduled_tokens_np)
|
||||
|
||||
pooler_output: list[dict[str, object]] = []
|
||||
for rid in req_ids_output_copy:
|
||||
idx = req_id_to_index_output_copy[rid]
|
||||
@@ -449,24 +449,3 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
)
|
||||
|
||||
return async_output
|
||||
|
||||
def _merge_additional_information_update(self, req_id: str, upd: dict) -> None:
|
||||
req_state = self.requests.get(req_id)
|
||||
if req_state is None:
|
||||
return
|
||||
existing = getattr(req_state, "additional_information_cpu", {})
|
||||
if not isinstance(existing, dict):
|
||||
existing = {}
|
||||
merged = dict(existing)
|
||||
for k, v in upd.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
merged[k] = v.detach().to("cpu").contiguous()
|
||||
elif isinstance(v, list):
|
||||
merged[k] = [
|
||||
(item.detach().to("cpu").contiguous() if isinstance(item, torch.Tensor) else item) for item in v
|
||||
]
|
||||
else:
|
||||
merged[k] = v
|
||||
setattr(req_state, "additional_information_cpu", merged)
|
||||
|
||||
# ===== Helper functions extracted for clarity and reuse =====
|
||||
@@ -662,25 +662,13 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
req_token_spans.append((start_offset, start_offset + sched_tokens))
|
||||
return req_token_spans
|
||||
|
||||
def _build_model_kwargs_extra(
|
||||
self,
|
||||
per_req_additional_information: dict[str, dict] | None,
|
||||
num_scheduled_tokens_np,
|
||||
) -> dict:
|
||||
def _build_model_kwargs_extra(self) -> dict:
|
||||
"""Build extra keyword arguments passed to the model for this step, including:
|
||||
- additional_information_by_req_id: per-request additional information provided by preprocess for this step
|
||||
- runtime_additional_information: per-request additional information stored in request state
|
||||
- request_ids: the request id list in batch order
|
||||
- request_token_spans: token spans per request within the flattened sequence
|
||||
"""
|
||||
model_kwargs_extra: dict[str, object] = {}
|
||||
if per_req_additional_information:
|
||||
model_kwargs_extra["additional_information_by_req_id"] = per_req_additional_information
|
||||
try:
|
||||
model_kwargs_extra["runtime_additional_information"] = self._gather_runtime_additional_information()
|
||||
model_kwargs_extra["request_ids"] = list(self.input_batch.req_ids)
|
||||
if num_scheduled_tokens_np is not None:
|
||||
model_kwargs_extra["request_token_spans"] = self._compute_request_token_spans(num_scheduled_tokens_np)
|
||||
except Exception as e:
|
||||
logger.error(f"[OMNI DEBUG] Error building model_kwargs_extra: {e}")
|
||||
import traceback
|
||||
@@ -688,49 +676,29 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
traceback.print_exc()
|
||||
return model_kwargs_extra
|
||||
|
||||
def _process_additional_information_updates(self, multimodal_outputs: object) -> None:
|
||||
def _process_additional_information_updates(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
multimodal_outputs: object,
|
||||
num_scheduled_tokens_np: np.ndarray,
|
||||
) -> None:
|
||||
"""Process model-provided per-request additional_information updates and merge into request state."""
|
||||
try:
|
||||
if multimodal_outputs is None:
|
||||
logger.debug("[OMNI] multimodal_outputs is None, no updates to process")
|
||||
return
|
||||
if not isinstance(multimodal_outputs, dict):
|
||||
logger.debug(f"[OMNI] multimodal_outputs is not dict: {type(multimodal_outputs)}")
|
||||
return
|
||||
if (
|
||||
"additional_information_update" not in multimodal_outputs
|
||||
and "additional_information_update_by_req_id" not in multimodal_outputs
|
||||
):
|
||||
logger.debug(f"[OMNI] multimodal_outputs has no update keys: {list(multimodal_outputs.keys())}")
|
||||
return
|
||||
updates_list = multimodal_outputs.get("additional_information_update")
|
||||
if isinstance(updates_list, list):
|
||||
logger.debug(f"[OMNI] Processing additional_information_update list with {len(updates_list)} items")
|
||||
for idx, upd in enumerate(updates_list):
|
||||
if not isinstance(upd, dict) or idx >= len(self.input_batch.req_ids):
|
||||
continue
|
||||
req_id = self.input_batch.req_ids[idx]
|
||||
self._merge_additional_information_update(req_id, upd)
|
||||
updates_map = multimodal_outputs.get("additional_information_update_by_req_id")
|
||||
if isinstance(updates_map, dict):
|
||||
logger.debug(
|
||||
f"[OMNI] Processing additional_information_update_by_req_id for {list(updates_map.keys())}"
|
||||
)
|
||||
for req_id, upd in updates_map.items():
|
||||
if not isinstance(upd, dict):
|
||||
continue
|
||||
if req_id not in self.requests:
|
||||
logger.warning(f"[OMNI] req_id {req_id} not found in requests")
|
||||
continue
|
||||
self._merge_additional_information_update(req_id, upd)
|
||||
|
||||
# execute the custom postprocess function
|
||||
# TODO(Peiqi): do we have a more elegant way to do this?
|
||||
if hasattr(self.model, "has_postprocess") and self.model.has_postprocess:
|
||||
for req_index, req_id in enumerate(self.input_batch.req_ids):
|
||||
req_state = self.requests.get(req_id)
|
||||
if req_state:
|
||||
info = getattr(req_state, "additional_information_cpu", {})
|
||||
if "thinker_reply_part_per_request" in info:
|
||||
q = info["thinker_reply_part_per_request"]
|
||||
if hasattr(q, "shape"):
|
||||
logger.debug(f"[OMNI] After update, req={req_id} queue shape: {q.shape}")
|
||||
req_infos = (
|
||||
getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
|
||||
)
|
||||
start_offset = int(self.query_start_loc.cpu[req_index])
|
||||
sched_tokens = int(num_scheduled_tokens_np[req_index])
|
||||
s, e = start_offset, start_offset + sched_tokens
|
||||
# only consider to store data into update dict.
|
||||
hidden_states_slice = hidden_states[s:e]
|
||||
update_dict = self.model.postprocess(hidden_states_slice, **req_infos)
|
||||
self._merge_additional_information_update(req_id, update_dict)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error merging for requests:{self.input_batch.req_ids} "
|
||||
@@ -741,46 +709,21 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
def _build_mm_inputs_and_overlays(
|
||||
self,
|
||||
scheduler_output: "SchedulerOutput",
|
||||
num_scheduled_tokens: int,
|
||||
num_scheduled_tokens_np: np.ndarray,
|
||||
) -> dict[str, dict]:
|
||||
"""Run MM encoder, fill scheduled inputs_embeds, and overlay prompt embeds;
|
||||
collect per-request additional_information for the prefill portion."""
|
||||
# Run the multimodal encoder if any.
|
||||
self._execute_mm_encoder(scheduler_output)
|
||||
mm_embeds = self._gather_mm_embeddings(scheduler_output)
|
||||
# Always use embeddings as inputs for MM models
|
||||
inputs_embeds_scheduled = self.model.get_input_embeddings(
|
||||
input_ids=self.input_ids.gpu[:num_scheduled_tokens],
|
||||
multimodal_embeddings=mm_embeds or None,
|
||||
)
|
||||
self.inputs_embeds.gpu[:num_scheduled_tokens].copy_(inputs_embeds_scheduled)
|
||||
# Reset per-step additional information collector (deprecated concat path)
|
||||
if hasattr(self, "_forward_additional_information"):
|
||||
self._forward_additional_information = None
|
||||
# Overlay and collect per-request info
|
||||
return self._collect_additional_information_for_prefill(num_scheduled_tokens_np)
|
||||
|
||||
def _collect_additional_information_for_prefill(
|
||||
self,
|
||||
num_scheduled_tokens_np: np.ndarray,
|
||||
) -> dict[str, dict]:
|
||||
"""Overlay per-request prompt_embeds for the prefill portion and collect
|
||||
additional_information slices for this step. Returns a map req_id -> dict."""
|
||||
per_req_additional_information: dict[str, dict] = {}
|
||||
for req_index, req_id in enumerate(self.input_batch.req_ids):
|
||||
req_state = self.requests[req_id]
|
||||
pe_cpu = getattr(req_state, "prompt_embeds_cpu", None)
|
||||
addi_cpu = getattr(req_state, "additional_information_cpu", None)
|
||||
num_computed_tokens = int(self.input_batch.num_computed_tokens_cpu[req_index])
|
||||
prompt_len = len(req_state.prompt_token_ids)
|
||||
prompt_remaining = max(0, prompt_len - num_computed_tokens)
|
||||
sched_tokens = int(num_scheduled_tokens_np[req_index])
|
||||
overlay_len = min(sched_tokens, prompt_remaining)
|
||||
if overlay_len <= 0 and not (isinstance(addi_cpu, dict) and addi_cpu):
|
||||
if overlay_len <= 0:
|
||||
continue
|
||||
if overlay_len > 0 and pe_cpu is not None:
|
||||
src = pe_cpu[num_computed_tokens : num_computed_tokens + overlay_len].to(
|
||||
@@ -788,18 +731,6 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
)
|
||||
start_offset = int(self.query_start_loc.cpu[req_index])
|
||||
self.inputs_embeds[start_offset : start_offset + overlay_len].copy_(src)
|
||||
if addi_cpu is not None and isinstance(addi_cpu, dict):
|
||||
req_info: dict[str, object] = {}
|
||||
for k, v in addi_cpu.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
req_info[k] = v.detach().to("cpu").contiguous()
|
||||
elif isinstance(v, list):
|
||||
req_info[k] = v
|
||||
else:
|
||||
req_info[k] = v
|
||||
if req_info:
|
||||
per_req_additional_information[req_id] = req_info
|
||||
return per_req_additional_information
|
||||
|
||||
def _preprocess(
|
||||
self,
|
||||
@@ -909,17 +840,36 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
)
|
||||
self._omni_num_scheduled_tokens_np = num_scheduled_tokens_np
|
||||
|
||||
# Always collect additional_information, not only in prefill.
|
||||
# _collect_additional_information_for_prefill handles prompt_embeds overlay during prefill,
|
||||
# but even in decode we need per_req_additional_information so that
|
||||
# _build_model_kwargs_extra can pass runtime_additional_information correctly.
|
||||
per_req_additional_information: dict[str, dict] = {}
|
||||
# Note: only prefill need collect additional_information for now.
|
||||
# Decode don't need per_req_additional_information anymore.
|
||||
if inputs_embeds is not None:
|
||||
# Prefill: overlay prompt_embeds and collect additional_information
|
||||
per_req_additional_information = self._collect_additional_information_for_prefill(num_scheduled_tokens_np)
|
||||
# Save for _model_forward regardless of prefill; runtime_additional_information
|
||||
# will be fetched from request state inside _build_model_kwargs_extra
|
||||
self._omni_per_req_additional_information = per_req_additional_information
|
||||
self._collect_additional_information_for_prefill(num_scheduled_tokens_np)
|
||||
|
||||
if hasattr(self.model, "has_preprocess") and self.model.has_preprocess:
|
||||
# Overlay custom prompt_embeds per request for the prompt portion;
|
||||
# collect additional_information (tensor/list) for prefill portion only
|
||||
for req_index, req_id in enumerate(self.input_batch.req_ids):
|
||||
req_state = self.requests.get(req_id)
|
||||
req_infos = getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
|
||||
|
||||
start_offset = int(self.query_start_loc.cpu[req_index])
|
||||
sched_tokens = int(num_scheduled_tokens_np[req_index])
|
||||
s, e = start_offset, start_offset + sched_tokens
|
||||
span_len = int(e) - int(s)
|
||||
|
||||
# call the custom process function
|
||||
req_input_ids, req_embeds, update_dict = self.model.preprocess(
|
||||
input_ids=input_ids[s:e], input_embeds=inputs_embeds[s:e], **req_infos
|
||||
)
|
||||
# TODO(Peiqi): the merge stage could move out from the critical path
|
||||
self._merge_additional_information_update(req_id, update_dict)
|
||||
|
||||
# update the inputs_embeds and input_ids
|
||||
seg_len = min(span_len, req_embeds.shape[0])
|
||||
inputs_embeds[s : s + seg_len] = req_embeds[:seg_len]
|
||||
if isinstance(req_input_ids, torch.Tensor) and req_input_ids.numel() == seg_len:
|
||||
input_ids[s : s + seg_len] = req_input_ids
|
||||
|
||||
return (
|
||||
input_ids,
|
||||
@@ -939,10 +889,7 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
**model_kwargs: dict[str, Any],
|
||||
):
|
||||
"""Inject omni-specific kwargs into forward and cache model output"""
|
||||
model_kwargs_extra = self._build_model_kwargs_extra(
|
||||
self._omni_per_req_additional_information,
|
||||
self._omni_num_scheduled_tokens_np,
|
||||
)
|
||||
model_kwargs_extra = self._build_model_kwargs_extra()
|
||||
|
||||
runtime_info = model_kwargs_extra.get("runtime_additional_information", [])
|
||||
if runtime_info:
|
||||
@@ -961,3 +908,22 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
# Cache model output so later sample_tokens can consume multimodal results.
|
||||
self._omni_last_model_output = model_output
|
||||
return model_output
|
||||
|
||||
def _merge_additional_information_update(self, req_id: str, upd: dict) -> None:
|
||||
req_state = self.requests.get(req_id)
|
||||
if req_state is None:
|
||||
return
|
||||
existing = getattr(req_state, "additional_information_cpu", {})
|
||||
if not isinstance(existing, dict):
|
||||
existing = {}
|
||||
merged = dict(existing)
|
||||
for k, v in upd.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
merged[k] = v.detach().to("cpu").contiguous()
|
||||
elif isinstance(v, list):
|
||||
merged[k] = [
|
||||
(item.detach().to("cpu").contiguous() if isinstance(item, torch.Tensor) else item) for item in v
|
||||
]
|
||||
else:
|
||||
merged[k] = v
|
||||
setattr(req_state, "additional_information_cpu", merged)
|
||||
Reference in new issue
Block a user