Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Sy03 <1370724210@qq.com>
305 lines
13 KiB
Python
305 lines
13 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""AuK (Tencent): instruction-driven speech generation and editing.
|
|
|
|
Stage 0 (``model_stage="encoder"``) runs the frozen Qwen2.5-Omni thinker as a
|
|
prefill-only encoder. The outputs of all decoder layers are fused with learned
|
|
softmax weights into one ``[tokens, hidden]`` text condition, emitted through
|
|
``multimodal_outputs["hidden_states"]["output"]`` for the diffusion stage
|
|
(``vllm_omni/diffusion/models/auk``). The stage samples exactly one token
|
|
(``max_tokens=1``, forced to EOS); that token is discarded downstream.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from collections.abc import Iterable, Mapping
|
|
from functools import cached_property
|
|
from itertools import islice
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
import torch.nn.functional as F
|
|
from safetensors import safe_open
|
|
from transformers import PretrainedConfig
|
|
from vllm.config import VllmConfig
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.models.interfaces import SupportsMRoPE, SupportsMultiModal, SupportsPP
|
|
from vllm.model_executor.models.utils import init_vllm_registered_model, maybe_prefix
|
|
from vllm.multimodal import MULTIMODAL_REGISTRY
|
|
from vllm.multimodal.inputs import MultiModalFeatureSpec
|
|
from vllm.sequence import IntermediateTensors
|
|
from vllm.v1.sample.sampler import Sampler
|
|
|
|
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
|
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni import Qwen2_5OmniForConditionalGeneration
|
|
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker import (
|
|
Qwen2_5OmniThinkerDummyInputsBuilder,
|
|
Qwen2_5OmniThinkerForConditionalGeneration,
|
|
Qwen2_5OmniThinkerMultiModalProcessor,
|
|
Qwen2_5OmniThinkerProcessingInfo,
|
|
)
|
|
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
# File next to the thinker shards in the assembled checkpoint directory that
|
|
# carries the DiT (``transformer.*``) and the two layer-fusion tensors.
|
|
AUK_WEIGHTS_FILE = "auk.safetensors"
|
|
LAYER_WEIGHTS_KEY = "layer_weights"
|
|
LAYER_SCALE_KEY = "layer_scale"
|
|
# The fused text condition travels as ``multimodal_outputs["hidden_states"]["output"]``
|
|
# (``HiddenStatesStruct.output``), the same stage-wire slot MiniMax H3 uses.
|
|
TEXT_COND_KEY = "output"
|
|
# ``F.layer_norm`` epsilon used by the upstream fusion (torch default).
|
|
_FUSION_LN_EPS = 1e-5
|
|
|
|
|
|
class AuKProcessingInfo(Qwen2_5OmniThinkerProcessingInfo):
|
|
"""Qwen2.5-Omni thinker processing restricted to AuK's single audio input.
|
|
|
|
AuK conditions on text plus at most one reference or source clip; images
|
|
and video are not part of its contract, so they are not advertised (and the
|
|
thinker then builds no vision tower).
|
|
"""
|
|
|
|
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
|
return {"audio": 1}
|
|
|
|
|
|
@MULTIMODAL_REGISTRY.register_processor(
|
|
Qwen2_5OmniThinkerMultiModalProcessor,
|
|
info=AuKProcessingInfo,
|
|
dummy_inputs=Qwen2_5OmniThinkerDummyInputsBuilder,
|
|
)
|
|
class AuKForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP, SupportsMRoPE):
|
|
"""Encoder stage of AuK: Qwen2.5-Omni thinker with learned layer fusion.
|
|
|
|
The forward pass re-implements the thinker language model's layer loop so
|
|
the per-layer outputs can be fused on the fly: vLLM's decoder layers carry
|
|
the residual stream separately, and ``hidden + residual`` after layer ``i``
|
|
is the HF ``output_hidden_states[i + 1]`` entry, while the last entry is
|
|
the final-normed state. Only one accumulator lives at a time.
|
|
"""
|
|
|
|
have_multimodal_outputs = True
|
|
# The fused condition is the payload; the runner's default last-hidden
|
|
# copy and prefix-cached hidden states are not needed (MiniMax H3 pattern).
|
|
omni_pooler_payload_include_hidden = False
|
|
requires_full_prefix_cached_hidden_states = False
|
|
|
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
|
super().__init__()
|
|
self.has_preprocess = False
|
|
self.vllm_config = vllm_config
|
|
config = vllm_config.model_config.hf_config
|
|
|
|
self.model_stage = getattr(vllm_config.model_config, "model_stage", None) or "encoder"
|
|
if self.model_stage != "encoder":
|
|
raise ValueError(
|
|
f"AuKForConditionalGeneration only hosts the 'encoder' stage, got model_stage={self.model_stage!r}; "
|
|
"the DiT stage is the AuKPipeline diffusion model."
|
|
)
|
|
|
|
thinker_config = config.thinker_config
|
|
self.thinker_config = thinker_config
|
|
self.thinker = init_vllm_registered_model(
|
|
vllm_config=vllm_config,
|
|
prefix=maybe_prefix(prefix, "thinker"),
|
|
hf_config=thinker_config,
|
|
architectures=["Qwen2_5OmniThinkerModel"],
|
|
)
|
|
# The audio-only mm limits keep the thinker from building its vision
|
|
# tower; drop it explicitly as well, as the upstream implementation does,
|
|
# so no vision weights are loaded or resident.
|
|
if getattr(self.thinker, "visual", None) is not None:
|
|
del self.thinker.visual
|
|
self.thinker.visual = None
|
|
self.model = self.thinker
|
|
self.make_empty_intermediate_tensors = self.thinker.make_empty_intermediate_tensors
|
|
|
|
num_layers = thinker_config.text_config.num_hidden_layers
|
|
# Filled by load_weights() from AUK_WEIGHTS_FILE; buffers so the vLLM
|
|
# loader does not expect them among the thinker shards.
|
|
self.register_buffer("layer_weights", torch.zeros(num_layers, dtype=torch.float32))
|
|
self.register_buffer("layer_scale", torch.ones(1, dtype=torch.float32))
|
|
self._fusion_loaded = False
|
|
|
|
# -------------------- delegations to the thinker --------------------
|
|
|
|
@classmethod
|
|
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
|
return Qwen2_5OmniThinkerForConditionalGeneration.get_placeholder_str(modality, i)
|
|
|
|
def get_language_model(self) -> nn.Module:
|
|
return self.thinker.get_language_model()
|
|
|
|
@cached_property
|
|
def sampler(self):
|
|
if hasattr(self.thinker, "sampler"):
|
|
return self.thinker.sampler
|
|
return Sampler()
|
|
|
|
def embed_input_ids(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
multimodal_embeddings=None,
|
|
is_multimodal=None,
|
|
) -> torch.Tensor:
|
|
return self.thinker.embed_input_ids(
|
|
input_ids=input_ids,
|
|
multimodal_embeddings=multimodal_embeddings,
|
|
is_multimodal=is_multimodal,
|
|
)
|
|
|
|
def embed_multimodal(self, **kwargs):
|
|
return self.thinker.embed_multimodal(**kwargs)
|
|
|
|
def get_mrope_input_positions(
|
|
self,
|
|
input_tokens: list[int],
|
|
mm_features: list[MultiModalFeatureSpec] | None = None,
|
|
*,
|
|
hf_config: PretrainedConfig,
|
|
image_grid_thw: list[list[int]] | torch.Tensor,
|
|
video_grid_thw: list[list[int]] | torch.Tensor,
|
|
second_per_grid_ts: list[float] | None = None,
|
|
context_len: int = 0,
|
|
seq_len: int | None = None,
|
|
audio_feature_lengths: torch.Tensor | None = None,
|
|
use_audio_in_video: bool = False,
|
|
) -> tuple[torch.Tensor, int]:
|
|
# The omni runner filters kwargs by this signature (the extended
|
|
# Qwen2.5-Omni form); the unified Qwen2.5-Omni class implements the
|
|
# audio-aware position math without touching instance state.
|
|
return Qwen2_5OmniForConditionalGeneration.get_mrope_input_positions(
|
|
self,
|
|
input_tokens,
|
|
mm_features,
|
|
hf_config=hf_config,
|
|
image_grid_thw=image_grid_thw,
|
|
video_grid_thw=video_grid_thw,
|
|
second_per_grid_ts=second_per_grid_ts,
|
|
context_len=context_len,
|
|
seq_len=seq_len,
|
|
audio_feature_lengths=audio_feature_lengths,
|
|
use_audio_in_video=use_audio_in_video,
|
|
)
|
|
|
|
def compute_logits(self, hidden_states: torch.Tensor | OmniOutput, **kwargs: object) -> torch.Tensor | None:
|
|
"""Emit EOS immediately; the useful result is the prefill text condition."""
|
|
if isinstance(hidden_states, OmniOutput):
|
|
hidden_states = hidden_states.text_hidden_states
|
|
text_config = self.thinker_config.text_config
|
|
logits = hidden_states.new_full(
|
|
(hidden_states.shape[0], int(text_config.vocab_size)),
|
|
torch.finfo(hidden_states.dtype).min,
|
|
)
|
|
logits[:, self._eos_token_id()] = 0
|
|
return logits
|
|
|
|
def _eos_token_id(self) -> int:
|
|
for cfg in (self.thinker_config.text_config, self.thinker_config, self.vllm_config.model_config.hf_config):
|
|
eos = getattr(cfg, "eos_token_id", None)
|
|
if isinstance(eos, (list, tuple)) and eos:
|
|
eos = eos[0]
|
|
if isinstance(eos, int):
|
|
return eos
|
|
# Qwen2.5-Omni tokenizers end turns with <|im_end|>.
|
|
return 151645
|
|
|
|
# -------------------- forward --------------------
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
intermediate_tensors: IntermediateTensors | None = None,
|
|
inputs_embeds: torch.Tensor | None = None,
|
|
**kwargs: object,
|
|
) -> OmniOutput:
|
|
if intermediate_tensors is not None:
|
|
raise NotImplementedError("AuK encoder does not support pipeline parallelism")
|
|
language_model = self.thinker.language_model.model
|
|
num_layers = len(language_model.layers)
|
|
if language_model.start_layer != 0 or language_model.end_layer != num_layers:
|
|
raise NotImplementedError("AuK encoder needs all decoder layers on one rank")
|
|
|
|
if inputs_embeds is None:
|
|
hidden_states = language_model.embed_input_ids(input_ids)
|
|
else:
|
|
hidden_states = inputs_embeds
|
|
residual: torch.Tensor | None = None
|
|
|
|
weights = torch.softmax(self.layer_weights, dim=0)
|
|
fused: torch.Tensor | None = None
|
|
normed_shape = (hidden_states.shape[-1],)
|
|
layers = islice(language_model.layers, language_model.start_layer, language_model.end_layer)
|
|
for idx, layer in enumerate(layers):
|
|
hidden_states, residual = layer(positions, hidden_states, residual)
|
|
if idx + 1 < num_layers:
|
|
layer_output = hidden_states + residual
|
|
else:
|
|
# HF's last hidden_states entry is post final norm.
|
|
hidden_states, _ = language_model.norm(hidden_states, residual)
|
|
layer_output = hidden_states
|
|
normed = F.layer_norm(layer_output.float(), normed_shape, eps=_FUSION_LN_EPS)
|
|
contribution = normed * weights[idx]
|
|
fused = contribution if fused is None else fused + contribution
|
|
|
|
assert fused is not None
|
|
text_cond = (fused * self.layer_scale).to(hidden_states.dtype)
|
|
# Stage-wire layout: [tokens, features] under hidden_states.output so
|
|
# the runner slices it per request (and concatenates across chunked
|
|
# prefill steps); the stage-1 processor reads the same path.
|
|
return OmniOutput(
|
|
text_hidden_states=hidden_states,
|
|
multimodal_outputs={"hidden_states": {"output": text_cond}},
|
|
)
|
|
|
|
# -------------------- weights --------------------
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
|
thinker_weights: list[tuple[str, torch.Tensor]] = []
|
|
skipped = 0
|
|
for name, tensor in weights:
|
|
if name.startswith("thinker."):
|
|
thinker_weights.append((name, tensor))
|
|
elif name.startswith(("talker.", "token2wav.", "transformer.")):
|
|
skipped += 1
|
|
else:
|
|
logger.warning("AuK encoder: ignoring unexpected weight %s", name)
|
|
if skipped:
|
|
logger.debug("AuK encoder: skipped %d non-thinker weights", skipped)
|
|
|
|
loaded = self.thinker.load_weights(thinker_weights)
|
|
loaded = add_prefix_to_loaded_weights(loaded, "thinker")
|
|
self._load_fusion_params()
|
|
return loaded
|
|
|
|
def _load_fusion_params(self) -> None:
|
|
model_dir = self.vllm_config.model_config.model
|
|
path = os.path.join(model_dir, AUK_WEIGHTS_FILE)
|
|
if not os.path.isfile(path):
|
|
raise FileNotFoundError(
|
|
f"AuK encoder: {AUK_WEIGHTS_FILE} not found in {model_dir!r}; assemble the checkpoint with "
|
|
"tools/prepare_auk_checkpoint.py"
|
|
)
|
|
with safe_open(path, framework="pt", device="cpu") as f:
|
|
layer_weights = f.get_tensor(LAYER_WEIGHTS_KEY).float()
|
|
layer_scale = f.get_tensor(LAYER_SCALE_KEY).float().reshape(1)
|
|
if layer_weights.shape != self.layer_weights.shape:
|
|
raise ValueError(
|
|
f"AuK encoder: {LAYER_WEIGHTS_KEY} has {tuple(layer_weights.shape)} entries, "
|
|
f"thinker has {tuple(self.layer_weights.shape)} layers"
|
|
)
|
|
self.layer_weights.copy_(layer_weights.to(self.layer_weights.device))
|
|
self.layer_scale.copy_(layer_scale.to(self.layer_scale.device))
|
|
self._fusion_loaded = True
|
|
logger.info(
|
|
"AuK encoder: loaded layer fusion (scale %.3f, top layer weight %.3f at layer %d)",
|
|
float(layer_scale[0]),
|
|
float(torch.softmax(layer_weights, 0).max()),
|
|
int(torch.softmax(layer_weights, 0).argmax()) + 1,
|
|
)
|