Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

486 lines
18 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Inference-only Qwen3-Omni thinker (text output only).
The thinker composes TokenSpeed's shared Qwen3 MoE language model, Qwen3 vision
tower, and Qwen3-Omni audio tower. Talker/Code2Wav weights are deliberately
ignored: this entry point implements ``return_audio=False`` and independent
image/audio/video inputs (``use_audio_in_video=False``).
"""
from __future__ import annotations
import logging
from collections.abc import Iterable
import torch
from tokenspeed.runtime.configs.qwen3_vision_config import Qwen3VLVisionConfig
from tokenspeed.runtime.distributed.mapping import Mapping
from tokenspeed.runtime.execution.context import ForwardContext
from tokenspeed.runtime.layers.logits_processor import LogitsMetadata
from tokenspeed.runtime.layers.moe import (
ExpertCheckpointSchema,
build_moe_checkpoint_loader,
)
from tokenspeed.runtime.layers.quantization.base_config import QuantizationConfig
from tokenspeed.runtime.layers.utils import get_layer_id
from tokenspeed.runtime.model_loader.weight_utils import default_weight_loader
from tokenspeed.runtime.models.qwen3_audio import Qwen3AudioEncoder
from tokenspeed.runtime.models.qwen3_moe import Qwen3MoeForCausalLM, Qwen3MoeModel
from tokenspeed.runtime.models.qwen3_vision import Qwen3VLMoeVisionModel
from tokenspeed.runtime.multimodal.embedder import (
EncoderSpec,
MultimodalEmbedder,
pad_input_tokens,
)
from tokenspeed.runtime.multimodal.encoder_cudagraph import (
EncoderCudaGraphWrapper,
VisionEncoderCudaGraphAdapter,
)
from tokenspeed.runtime.multimodal.inputs import (
Modality,
MultimodalDataItem,
MultimodalInputs,
)
from tokenspeed.runtime.utils.env import envs
logger = logging.getLogger(__name__)
def _get_thinker_config(config):
return getattr(config, "thinker_config", config)
def _shared_vision_config(vision_config) -> Qwen3VLVisionConfig:
if not getattr(vision_config, "apply_vit_abs_pos_embed", True):
raise ValueError("Qwen3-Omni without absolute vision positions is unsupported")
values = (
vision_config.to_dict()
if hasattr(vision_config, "to_dict")
else dict(vars(vision_config))
)
patch_size = int(values.get("patch_size", 16))
image_size = values.get("image_size")
if image_size is not None:
image_size = int(image_size)
if image_size % patch_size:
raise ValueError("Qwen3-Omni image_size must be divisible by patch_size")
expected_positions = (image_size // patch_size) ** 2
configured_positions = values.get("num_position_embeddings")
if (
configured_positions is not None
and int(configured_positions) != expected_positions
):
raise ValueError(
"Qwen3-Omni image_size/patch_size disagrees with "
"num_position_embeddings"
)
values["num_position_embeddings"] = expected_positions
values["deepstack_visual_indexes"] = (
values.get("deepstack_visual_indexes", [8, 16, 24]) or []
)
return Qwen3VLVisionConfig(**values)
class Qwen3OmniMoeTextModel(Qwen3MoeModel):
"""Qwen3 MoE decoder with Omni visual deepstack injection."""
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
input_embeds: torch.Tensor | None = None,
input_deepstack_embeds: torch.Tensor | None = None,
) -> tuple[torch.Tensor, None]:
hidden_states = (
self.embed_tokens(input_ids) if input_embeds is None else input_embeds
)
residual = None
hidden_size = self.config.hidden_size
num_deepstack = (
input_deepstack_embeds.shape[-1] // hidden_size
if input_deepstack_embeds is not None
else 0
)
for layer_idx, layer in enumerate(self.layers):
hidden_states, residual = layer(
positions,
hidden_states,
ctx,
out_cache_loc,
residual,
cos_sin=None,
)
if layer_idx < num_deepstack and input_deepstack_embeds.numel() > 0:
start = layer_idx * hidden_size
hidden_states.add_(
input_deepstack_embeds[:, start : start + hidden_size]
)
if not ctx.forward_mode.is_idle():
hidden_states, _ = layer.comm_manager.final_norm(
hidden_states, residual, ctx, self.norm
)
return hidden_states, None
class Qwen3OmniMoeForConditionalGeneration(Qwen3MoeForCausalLM):
"""Qwen3-Omni thinker for text-only generation."""
model_cls = Qwen3OmniMoeTextModel
def __init__(
self,
config,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
is_multimodal_active: bool = True,
mm_attention_backend: str | None = None,
) -> None:
self.omni_config = config
self.thinker_config = _get_thinker_config(config)
text_config = self.thinker_config.text_config
super().__init__(config=text_config, mapping=mapping, quant_config=quant_config)
self.is_multimodal_active = is_multimodal_active
self.multimodal_embedder = (
MultimodalEmbedder() if is_multimodal_active else None
)
if not is_multimodal_active:
self.visual = None
self.audio_tower = None
self.deepstack_visual_indexes = []
self.num_deepstack_embeddings = 0
self.image_encoder = None
self.video_encoder = None
self.audio_encoder = None
return
vision_config = _shared_vision_config(self.thinker_config.vision_config)
if vision_config.out_hidden_size != text_config.hidden_size:
raise ValueError(
"Qwen3-Omni vision output size must match text hidden size"
)
if self.thinker_config.audio_config.output_dim != text_config.hidden_size:
raise ValueError("Qwen3-Omni audio output size must match text hidden size")
self.visual = Qwen3VLMoeVisionModel(
vision_config,
mapping=mapping,
quant_config=None,
norm_eps=getattr(text_config, "rms_norm_eps", 1e-6),
prefix="visual",
mm_attention_backend=mm_attention_backend,
)
self.audio_tower = Qwen3AudioEncoder(
self.thinker_config.audio_config,
mapping=mapping,
quant_config=None,
prefix="audio_tower",
mm_attention_backend=mm_attention_backend,
)
self.deepstack_visual_indexes = self.visual.deepstack_visual_indexes
self.num_deepstack_embeddings = len(self.deepstack_visual_indexes)
# Encoder callables can be replaced by CUDA graph wrappers at runtime.
self.image_encoder = self.get_image_feature
self.video_encoder = self.get_video_feature
self.audio_encoder = self.audio_tower.encode
def separate_deepstack_embeds(
self, embedding: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
expected_parts = 1 + self.num_deepstack_embeddings
if embedding.shape[-1] != self.config.hidden_size * expected_parts:
raise ValueError(
f"vision embedding width {embedding.shape[-1]} does not match "
f"{expected_parts} x text hidden size {self.config.hidden_size}"
)
split = self.config.hidden_size
return embedding[:, :split], embedding[:, split:]
def pad_input_ids(
self, input_ids: list[int], mm_inputs: MultimodalInputs
) -> list[int]:
return pad_input_tokens(input_ids, mm_inputs)
def pre_encode(
self, items: list[MultimodalDataItem]
) -> tuple[torch.Tensor, torch.Tensor]:
pixel_values = torch.cat([item.feature for item in items], dim=0).type(
self.visual.dtype
)
grid = torch.cat(
[
(
item.video_grid_thw
if item.modality == Modality.VIDEO
else item.image_grid_thw
)
for item in items
],
dim=0,
)
if pixel_values.dim() != 2 or grid.dim() != 2:
raise ValueError("Qwen3-Omni vision features require 2-D patches and grids")
return self.visual.prepare_patch_embed(pixel_values, grid), grid
def post_encode(
self, encoder_outs: list[torch.Tensor], grid: torch.Tensor
) -> torch.Tensor:
del grid
return torch.cat(encoder_outs, dim=0)
def get_image_feature(self, items: list[MultimodalDataItem]) -> torch.Tensor:
tokens, grid = self.pre_encode(items)
output = self.visual.forward_blocks(tokens, self.visual.prepare_metadata(grid))
return self.post_encode([output], grid)
def get_video_feature(self, items: list[MultimodalDataItem]) -> torch.Tensor:
tokens, grid = self.pre_encode(items)
output = self.visual.forward_blocks(tokens, self.visual.prepare_metadata(grid))
return self.post_encode([output], grid)
def _build_encoder_cudagraph_wrapper(
self,
mapping: Mapping,
*,
max_metadata_sequences_per_batch: int | None = None,
metadata_sequence_budget_from_encoder_output_budget: bool = False,
) -> EncoderCudaGraphWrapper:
adapter = VisionEncoderCudaGraphAdapter(
tower=self.visual,
pre_encode=self.pre_encode,
post_encode=self.post_encode,
out_div=self.visual.spatial_merge_size**2,
merge=self.visual.spatial_merge_size,
input_feature_shape=(1, self.visual.hidden_size),
modality_name="vision",
capture_tp_size=mapping.vision.tp_size,
capture_tp_group=mapping.vision.tp_group,
)
return EncoderCudaGraphWrapper(
adapter=adapter,
budget_range=(64, 4096),
max_metadata_sequences_per_batch=max_metadata_sequences_per_batch,
metadata_sequence_budget_from_encoder_output_budget=(
metadata_sequence_budget_from_encoder_output_budget
),
)
def make_encoder_cudagraph_wrappers(self, mapping: Mapping) -> dict:
max_video_sequences = (
envs.TOKENSPEED_MM_VIDEO_ENCODER_CUDA_GRAPH_MAX_SEQUENCES_PER_BATCH.get()
)
if max_video_sequences is not None:
max_video_sequences = max(1, max_video_sequences)
shared = self._build_encoder_cudagraph_wrapper(
mapping,
max_metadata_sequences_per_batch=max_video_sequences,
metadata_sequence_budget_from_encoder_output_budget=(
max_video_sequences is None
),
)
return {"image_encoder": shared, "video_encoder": shared}
@torch.no_grad()
def forward(
self,
ctx: ForwardContext,
input_ids: torch.Tensor,
positions: torch.Tensor,
out_cache_loc: torch.Tensor,
**kwargs,
) -> torch.Tensor:
multimodal_context = kwargs.pop("multimodal_context", None)
if (
not self.is_multimodal_active
and multimodal_context is not None
and multimodal_context.has_extend_inputs()
):
raise RuntimeError(
"Qwen3-Omni received multimodal inputs while its encoders are disabled"
)
if (
multimodal_context is None
or not multimodal_context.has_extend_inputs()
or ctx.forward_mode.is_decode_or_idle()
):
return super().forward(ctx, input_ids, positions, out_cache_loc, **kwargs)
input_embeds, model_kwargs = self.multimodal_embedder.apply(
input_ids=input_ids,
text_embedding=self.model.embed_tokens,
ctx=multimodal_context,
encoders={
Modality.IMAGE: EncoderSpec(self.image_encoder, deepstack=True),
Modality.VIDEO: EncoderSpec(self.video_encoder, deepstack=True),
Modality.AUDIO: EncoderSpec(self.audio_encoder),
},
multimodal_model=self,
is_decode_or_idle=ctx.forward_mode.is_decode_or_idle(),
)
hidden_states, aux_hidden_states = self.model(
input_ids,
positions,
ctx,
out_cache_loc,
input_embeds=input_embeds,
**model_kwargs,
)
return self.logits_processor(
input_ids,
hidden_states,
self.lm_head,
LogitsMetadata.from_forward_context(ctx),
aux_hidden_states,
)
@staticmethod
def _map_visual_weight(name: str) -> str:
name = name.replace("attn.qkv.", "attn.qkv_proj.")
name = name.replace("merger_list.", "deepstack_merger_list.")
name = name.replace(".ln_q.", ".norm.")
name = name.replace(".mlp.0.", ".linear_fc1.")
name = name.replace(".mlp.2.", ".linear_fc2.")
return name
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
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),
]
ignore_suffixes = (
".bias",
"_bias",
".k_scale",
"_k_scale",
".v_scale",
"_v_scale",
".weight_scale",
"_weight_scale",
".input_scale",
"_input_scale",
)
params = dict(self.named_parameters(remove_duplicate=False))
moe_loader = build_moe_checkpoint_loader(
params_dict=params,
expert_schema=ExpertCheckpointSchema(
gate_proj_name="gate_proj",
down_proj_name="down_proj",
up_proj_name="up_proj",
),
fused_schema=ExpertCheckpointSchema(
gate_up_fused_name="gate_up_proj",
down_proj_name="down_proj",
),
num_experts=self.config.num_experts,
ep_rank=self.mapping.moe.ep_rank,
ep_size=self.mapping.moe.ep_size,
)
loaded = set()
for original_name, loaded_weight in weights:
if original_name.startswith(("talker.", "code2wav.")):
continue
name = (
original_name[len("thinker.") :]
if original_name.startswith("thinker.")
else original_name
)
if name.startswith("visual."):
if not self.is_multimodal_active:
continue
name = self._map_visual_weight(name)
if name not in params:
logger.warning("Parameter %s not found in Qwen3-Omni", name)
continue
param = params[name]
loader = getattr(param, "weight_loader", default_weight_loader)
loader(param, loaded_weight)
loaded.add(name)
continue
if name.startswith("audio_tower."):
if not self.is_multimodal_active:
continue
loaded_name = self.audio_tower.load_weight(name, loaded_weight)
if loaded_name is None:
logger.warning("Parameter %s not found in Qwen3-Omni", name)
else:
loaded.add(f"audio_tower.{loaded_name}")
continue
layer_id = get_layer_id(name)
if (
layer_id is not None
and hasattr(self.model, "start_layer")
and not self.model.start_layer <= layer_id < self.model.end_layer
):
continue
if "rotary_emb" in name:
continue
if self.config.tie_word_embeddings and name == "lm_head.weight":
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name or "mlp.experts" in name:
continue
mapped_name = name.replace(weight_name, param_name)
if mapped_name.endswith(ignore_suffixes) and mapped_name not in params:
break
if mapped_name not in params:
break
param = params[mapped_name]
param.weight_loader(param, loaded_weight, shard_id)
loaded.add(mapped_name)
break
else:
if name.endswith((".bias", "_bias")) and name not in params:
continue
if moe_loader.matches(name):
loaded.add(moe_loader.load(name, loaded_weight))
continue
if moe_loader.is_expert_checkpoint_weight(name):
continue
if name.endswith(ignore_suffixes) and name not in params:
continue
if name not in params:
logger.warning("Parameter %s not found in Qwen3-Omni", name)
continue
param = params[name]
loader = getattr(param, "weight_loader", default_weight_loader)
loader(param, loaded_weight)
loaded.add(name)
return loaded
EntryClass = [Qwen3OmniMoeForConditionalGeneration]