# Copyright (c) ModelScope Contributors. All rights reserved. import inspect import json import numpy as np import os import shutil import torch import torch.nn.functional as F import transformers from dataclasses import dataclass, field from functools import partial from packaging import version from PIL import Image from torch import nn from transformers.integrations import is_deepspeed_zero3_enabled from typing import Any, Dict, List, Literal, Optional from swift.utils import get_env_args, get_packed_seq_params, is_deepspeed_enabled, to_float_dtype from ..base import Template from ..constant import LLMTemplateType, MLLMTemplateType from ..register import register_template from ..template_inputs import StdTemplateInputs from ..template_meta import TemplateMeta from ..utils import Context, Word, findall from ..vision_utils import load_audio, load_batch, load_video_ovis2, load_video_ovis2_5 from .llama import Llama3TemplateMeta from .utils import DEFAULT_SYSTEM, ChatmlTemplateMeta @dataclass class QwenTemplateMeta(ChatmlTemplateMeta): default_system: Optional[str] = DEFAULT_SYSTEM auto_add_bos: bool = False stop_words: List[Word] = field(default_factory=lambda: ['<|endoftext|>']) agent_template: str = 'hermes' @dataclass class Qwen2_5TemplateMeta(QwenTemplateMeta): default_system: Optional[str] = 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' @dataclass class Qwen2_5MathTemplateMeta(QwenTemplateMeta): default_system: Optional[str] = 'Please reason step by step, and put your final answer within \\boxed{}.' qwq_preview_system = ('You are a helpful and harmless assistant. You are Qwen developed by Alibaba. ' 'You should think step-by-step.') register_template(QwenTemplateMeta(LLMTemplateType.qwen)) register_template(Qwen2_5TemplateMeta(LLMTemplateType.qwen2_5)) register_template(QwenTemplateMeta(LLMTemplateType.qwq_preview, default_system=qwq_preview_system)) register_template( QwenTemplateMeta(LLMTemplateType.qwq, default_system=None, is_thinking=True, thinking_prefix='\n')) @dataclass class Qwen3MixedTemplateMeta(QwenTemplateMeta): default_system: Optional[str] = None non_thinking_prefix: str = '\n\n\n\n' register_template(Qwen3MixedTemplateMeta(LLMTemplateType.qwen3, is_thinking=True)) QWEN3_GUARD_TEMPLATE = ( '<|im_start|>user\n' "# Task:\n1) Evaluate whether THE LAST USER's query belongs to safe or unsafe content based on our " 'safety policy.\n2) Identify the category of unsafe content found.\n\n\n' "**Safety: Safe**: The USER's query does not contain any prohibited content or content that could " "lead to prohibited content.\n**Safety: Unsafe**: The USER's query contains prohibited content.\n" "**Safety: Controversial**: The USER's query does not explicitly contain prohibited content, but " 'its intent, context, or potential responses could be misinterpreted, misused, or exploited to ' 'generate unsafe content under certain conditions.\n\n\n\nViolent.\nNon-violent Illegal Acts.\nSexual Content or Sexual Acts.\nPII.\nSuicide ' '& Self-Harm.\nUnethical Acts.\nPolitically Sensitive Topics.\nCopyright Violation.\nJailbreak.\n' '\n\n\n\nUSER: {{QUERY}}\n\n' "\n\nProvide your safety assessment for ONLY THE LAST **USER's query** in the above conversation:\n" " - The first line must be one of: 'Safety: Safe', 'Safety: Unsafe', 'Safety: Controversial'.\n - " "The second line should start with 'Categories:' followed by a list of any unsafe content " "categories, separated by commas. If the content is safe, use 'Categories: None'." '<|im_end|>\n<|im_start|>assistant\n') register_template(Qwen3MixedTemplateMeta( LLMTemplateType.qwen3_guard, prompt=[QWEN3_GUARD_TEMPLATE], )) register_template( QwenTemplateMeta( LLMTemplateType.qwen3_thinking, default_system=None, is_thinking=True, thinking_prefix='\n')) register_template(QwenTemplateMeta(LLMTemplateType.qwen3_nothinking, default_system=None)) register_template(QwenTemplateMeta(LLMTemplateType.qwen3_coder, default_system=None, agent_template='qwen3_coder')) class Qwen3EmbTemplate(Template): def _preprocess_inputs(self, inputs: StdTemplateInputs) -> None: super()._preprocess_inputs(inputs) if inputs.system is not None: inputs.messages[0]['content'] = inputs.system + ' ' + inputs.messages[0]['content'] inputs.system = None if len(inputs.messages) % 2 == 1 and inputs.messages[-1]['role'] != 'assistant': inputs.messages.append({'role': 'assistant', 'content': ''}) return inputs register_template( TemplateMeta( LLMTemplateType.qwen3_emb, template_cls=Qwen3EmbTemplate, suffix=['<|endoftext|>'], prefix=[], chat_sep=[], prompt=['{{QUERY}}'])) class Qwen3RerankerTemplate(Template): instruction = 'Given a web search query, retrieve relevant passages that answer the query' def _preprocess_inputs(self, inputs: StdTemplateInputs) -> None: super()._preprocess_inputs(inputs) if inputs.system is not None: instruction = inputs.system inputs.system = None else: instruction = self.instruction query = inputs.messages[0]['content'] document = inputs.messages[1]['content'] user_message = ': ' + instruction + '\n' + ': ' + query + '\n' + ': ' + document inputs.messages = [{'role': 'user', 'content': user_message}] return inputs def prepare_engine_kwargs(self) -> Dict[str, Any]: if self.mode == 'vllm': return { 'hf_overrides': { 'architectures': ['Qwen3ForSequenceClassification'], 'classifier_from_token': ['no', 'yes'], 'is_original_qwen3_reranker': True, } } else: return super().prepare_engine_kwargs() qwen3_reranker_system = ( 'Judge whether the Document meets the requirements based on the Query and the Instruct provided. ' 'Note that the answer can only be "yes" or "no".') register_template( Qwen3MixedTemplateMeta( LLMTemplateType.qwen3_reranker, default_system=qwen3_reranker_system, template_cls=Qwen3RerankerTemplate, agent_template=None)) register_template(Qwen2_5MathTemplateMeta(LLMTemplateType.qwen2_5_math)) class QwenPRMTemplate(Template): cot_process_placeholder = '' def _preprocess_inputs( self, inputs: StdTemplateInputs, ) -> None: super()._preprocess_inputs(inputs) total_content = '\n'.join([message['content'] or '' for message in inputs.messages]) if self.cot_process_placeholder not in total_content: inputs.messages[-1]['content'] = inputs.messages[-1]['content'] + self.cot_process_placeholder @staticmethod def make_step_rewards(logits, token_masks): probabilities = F.softmax(logits, dim=-1) probabilities = probabilities * token_masks.unsqueeze(-1) # bs, seq_len, num_labels all_scores_res = [] for i in range(probabilities.size(0)): sample = probabilities[i] # seq_len, num_labels positive_probs = sample[sample != 0].view(-1, 2)[:, 1] # valid_tokens, num_labels non_zero_elements_list = positive_probs.cpu().tolist() all_scores_res.append(non_zero_elements_list) return all_scores_res def decode_prm(self, input_ids: torch.Tensor, logits: torch.Tensor) -> Any: step_sep_id = self.tokenizer.encode(self.cot_process_placeholder)[0] token_masks = (input_ids == step_sep_id) return self.make_step_rewards(logits, token_masks) register_template(Qwen2_5MathTemplateMeta(LLMTemplateType.qwen2_5_math_prm, template_cls=QwenPRMTemplate)) class QwenVLTemplate(Template): load_images = False @staticmethod def _load_image(image, load_images: bool): if not load_images and isinstance(image, str) and (image.startswith('data:') or len(image) > 200): load_images = True return Template._load_image(image, load_images) def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: assert media_type == 'image' if self.mode == 'lmdeploy': return [f'Picture {index + 1}: ', [-100], '\n'] else: image = inputs.images[index] if self.mode == 'vllm': return [f'Picture {index + 1}: \n'] else: assert isinstance(image, str) return [f'Picture {index + 1}: {image}\n'] def replace_ref(self, ref: str, index: int, inputs: StdTemplateInputs) -> List[Context]: return [f'{ref}'] def replace_bbox(self, bbox: List[int], index: int, inputs: StdTemplateInputs) -> List[Context]: return [f'{self._get_bbox_str(bbox)}'] register_template(QwenTemplateMeta(MLLMTemplateType.qwen_vl, template_cls=QwenVLTemplate, agent_template=None)) class QwenAudioTemplate(Template): def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: assert media_type == 'audio' audios = inputs.audios audio = audios[index] assert isinstance(audio, str) return [f'Audio {index + 1}:\n'] def _tokenize(self, context, **kwargs): audio_info = self.processor.process_audio(context) return super()._tokenize(context, audio_info=audio_info) def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = super()._encode(inputs) text = ''.join([f'' for audio in inputs.audios]) audio_info = self.processor.process_audio(text) if audio_info: tokenizer_kwargs = {'audio_info': audio_info} encoded.update(tokenizer_kwargs) encoded['tokenizer_kwargs'] = tokenizer_kwargs return encoded def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]: res = super()._data_collator(batch, padding_to=padding_to) if batch[0].get('audio_info') is not None: res['audio_info'] = [b['audio_info'] for b in batch] return res register_template(QwenTemplateMeta(MLLMTemplateType.qwen_audio, template_cls=QwenAudioTemplate, agent_template=None)) class Qwen2AudioTemplate(Template): def init_env_args(self) -> None: super().init_env_args() self.sampling_rate = get_env_args('sampling_rate', int, self.processor.feature_extractor.sampling_rate) def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: assert media_type == 'audio' if not self.use_chat_template: return ['<|audio_bos|><|AUDIO|><|audio_eos|>\n'] else: return [f'Audio {index + 1}: <|audio_bos|><|AUDIO|><|audio_eos|>\n'] def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = super()._encode(inputs) sampling_rate = inputs.chat_template_kwargs.get('sampling_rate') if sampling_rate is None: sampling_rate = self.sampling_rate if inputs.audios: audios = load_batch(inputs.audios, load_func=partial(load_audio, sampling_rate=sampling_rate)) audio_inputs = self.processor.feature_extractor( audios, sampling_rate=sampling_rate, return_attention_mask=True, return_tensors='pt') audio_inputs['feature_attention_mask'] = audio_inputs.pop('attention_mask') encoded.update(audio_inputs) return encoded def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]: res = super()._data_collator(batch, padding_to=padding_to) input_features = [b['input_features'] for b in batch if b.get('input_features') is not None] feature_attention_mask = [ b['feature_attention_mask'] for b in batch if b.get('feature_attention_mask') is not None ] if input_features: res['input_features'] = torch.concat(input_features) res['feature_attention_mask'] = torch.concat(feature_attention_mask) return res register_template(QwenTemplateMeta(MLLMTemplateType.qwen2_audio, template_cls=Qwen2AudioTemplate)) class Qwen2VLTemplate(Template): image_token_id = 151655 video_token_id = 151656 placeholder_tokens = ['<|image_pad|>', '<|video_pad|>'] version = 'v2' use_model = True support_padding_free = True _requires_mm_token_type_ids = True def init_env_args(self): super().init_env_args() self.transformers_version = version.parse(transformers.__version__) self.bbox_format = get_env_args('QWENVL_BBOX_FORMAT', str, 'legacy') self.transformers_5_3 = self.transformers_version >= version.parse('5.3.0') self.transformers_5_9 = self.transformers_version >= version.parse('5.9.0') @property def requires_mm_token_type_ids(self): return self.transformers_5_3 and self._requires_mm_token_type_ids def _get_max_pixels(self, inputs=None): return self.max_pixels def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: from qwen_vl_utils import fetch_image, fetch_video assert media_type in {'image', 'video'} kwargs = {'image_patch_size': self.processor.image_processor.patch_size} if self.version == 'v3' else {} if self.mode == 'vllm': # resized in qwen_vl_utils, no need to resize again in vllm # ref: https://github.com/modelscope/ms-swift/issues/8445 inputs.mm_processor_kwargs['do_resize'] = False if media_type == 'image': inputs.images[index] = fetch_image({'image': inputs.images[index], **inputs.chat_template_kwargs}, **kwargs) if self.mode == 'lmdeploy': return ['<|vision_start|>', [-100], '<|vision_end|>'] else: return ['<|vision_start|><|image_pad|><|vision_end|>'] else: if self.version == 'v3': kwargs['return_video_metadata'] = True video = inputs.videos[index] video_inputs = {'video': video, **inputs.chat_template_kwargs} if isinstance(video, list): # image list from qwen_vl_utils import vision_process video_inputs['sample_fps'] = vision_process.FPS video, video_kwargs = fetch_video(video_inputs, return_video_sample_fps=True, **kwargs) tokens = ['<|vision_start|><|video_pad|><|vision_end|>'] if self.version == 'v2_5': inputs.mm_processor_kwargs.setdefault('fps', []).append(video_kwargs) elif self.version == 'v3': if self.mode != 'vllm': video, video_metadata = video inputs.mm_processor_kwargs.setdefault('video_metadata', []).append(video_metadata) tokens = ['<|video_pad|>'] inputs.mm_processor_kwargs['do_sample_frames'] = False if isinstance(video, torch.Tensor): video = video.to(torch.uint8) inputs.videos[index] = video return tokens def replace_ref(self, ref: str, index: int, inputs: StdTemplateInputs) -> List[Context]: if self.bbox_format == 'legacy': return [f'<|object_ref_start|>{ref}<|object_ref_end|>'] else: return [ref] def replace_bbox(self, bbox: List[int], index: int, inputs: StdTemplateInputs) -> List[Context]: if self.bbox_format == 'legacy': return [f'<|box_start|>{self._get_bbox_str(bbox)}<|box_end|>'] else: return [str(bbox)] def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = super()._encode(inputs) processor = self.processor input_ids = encoded['input_ids'] labels = encoded['labels'] loss_scale = encoded.get('loss_scale', None) mm_mask = [False] * len(input_ids) for media_type in ['images', 'videos']: mm_data = getattr(inputs, media_type) if mm_data: if media_type == 'images': media_token = self.image_token_id media_inputs = processor.image_processor(images=mm_data, return_tensors='pt', do_resize=False) media_grid_thw = media_inputs['image_grid_thw'] else: kwargs = {} if hasattr(processor, 'video_processor'): processor_func = processor.video_processor else: processor_func = processor.image_processor kwargs['images'] = None media_inputs = processor_func(videos=mm_data, return_tensors='pt', do_resize=False, **kwargs) media_grid_thw = media_inputs['video_grid_thw'] media_token = self.video_token_id if self.version == 'v2_5': fps = inputs.mm_processor_kwargs['fps'] media_inputs['second_per_grid_ts'] = [ processor.image_processor.temporal_patch_size / tmp for tmp in fps ] idx_list = findall(input_ids, media_token) merge_length = processor.image_processor.merge_size**2 def _get_new_tokens(i): token_len = (media_grid_thw[i].prod() // merge_length) return [media_token] * token_len input_ids, labels, loss_scale, mm_mask = self._extend_tokens( input_ids, labels, loss_scale, idx_list, _get_new_tokens, mm_mask=mm_mask) encoded.update(media_inputs) encoded['input_ids'] = input_ids encoded['labels'] = labels encoded['loss_scale'] = loss_scale if self.requires_mm_token_type_ids and any(mm_mask): encoded['mm_token_type_ids'] = self.create_mm_token_type_ids(input_ids, mm_mask) return encoded def forward_context(self, model, inputs): if not self.padding_free or self.transformers_version >= version.parse('4.53.0.dev'): return super().forward_context(model, inputs) text_position_ids = inputs['text_position_ids'] if self.version == 'v2': from transformers.models.qwen2_vl import modeling_qwen2_vl as modeling_module elif self.version == 'v2_5': from transformers.models.qwen2_5_vl import modeling_qwen2_5_vl as modeling_module elif self.version == 'omni_v2_5': from transformers.models.qwen2_5_omni import modeling_qwen2_5_omni as modeling_module return self._patch_flash_attention_forward(modeling_module, text_position_ids) def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]: if not self.is_training: return inputs input_ids = inputs['input_ids'] base_model = self.get_base_model(model) if hasattr(base_model.model, 'embed_tokens'): inputs_embeds = base_model.model.embed_tokens(input_ids) else: inputs_embeds = base_model.model.language_model.embed_tokens(input_ids) inputs_embeds = self._get_inputs_embeds_hf(inputs_embeds, inputs, model.visual, self.processor, model.config) return {'inputs_embeds': inputs_embeds} def _data_collator_mm_data(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: res = super()._data_collator_mm_data(batch) second_per_grid_ts = self.gather_list(batch, 'second_per_grid_ts') if second_per_grid_ts: res['second_per_grid_ts'] = second_per_grid_ts return res def packing_row(self, row: List[Dict[str, Any]]) -> Dict[str, Any]: for r in row: r_copy = r.copy() r_copy['input_ids'] = torch.tensor(r_copy['input_ids'])[None] if 'mm_token_type_ids' in r_copy: r_copy['mm_token_type_ids'] = r_copy['mm_token_type_ids'][None] r.update(self._get_position_ids(r_copy)) packed = super().packing_row(row) return packed def _get_get_rope_index(self): base_model = self.get_base_model(self._get_model()) if hasattr(base_model, 'get_rope_index'): get_rope_index = base_model.get_rope_index else: get_rope_index = base_model.model.get_rope_index return get_rope_index def _get_position_ids(self, inputs: Dict[str, Any]): # fix https://github.com/huggingface/transformers/pull/33487 kwargs = {} if self.version == 'v2_5': kwargs = {'second_per_grid_ts': inputs.get('second_per_grid_ts')} attention_mask = inputs.get('attention_mask_2d') if attention_mask is None: attention_mask = inputs.get('attention_mask') input_ids = inputs['input_ids'] mm_token_type_ids = inputs.get('mm_token_type_ids') if mm_token_type_ids is not None: kwargs['mm_token_type_ids'] = mm_token_type_ids position_ids, _ = self._get_get_rope_index()( input_ids, image_grid_thw=inputs.get('image_grid_thw'), video_grid_thw=inputs.get('video_grid_thw'), attention_mask=attention_mask, **kwargs) return {'position_ids': self._concat_text_position_ids(position_ids)} def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]: if self.requires_mm_token_type_ids: for b in batch: if 'input_ids' in b and 'mm_token_type_ids' not in b: b['mm_token_type_ids'] = torch.zeros(len(b['input_ids']), dtype=torch.int64) res = super()._data_collator(batch, padding_to=padding_to) if not self.padding_free: res.update(self._get_position_ids(res)) if 'position_ids' in res and self.is_training: position_ids = res['position_ids'] res['position_ids'] = position_ids[1:] res['text_position_ids'] = text_position_ids = position_ids[0] if self.transformers_version >= version.parse('4.53.0.dev') and text_position_ids.shape[0] == 1: # https://github.com/huggingface/transformers/pull/40194 res.update(get_packed_seq_params(text_position_ids)) return res register_template(QwenTemplateMeta(MLLMTemplateType.qwen2_vl, template_cls=Qwen2VLTemplate)) register_template( QwenTemplateMeta( MLLMTemplateType.qvq, default_system=('You are a helpful and harmless assistant. You are Qwen developed by Alibaba. ' 'Answer in the language of the question. You should think step-by-step.'), template_cls=Qwen2VLTemplate, )) class Qwen2_5VLTemplate(Qwen2VLTemplate): version = 'v2_5' norm_bbox = 'none' register_template(QwenTemplateMeta(MLLMTemplateType.qwen2_5_vl, template_cls=Qwen2_5VLTemplate)) register_template( QwenTemplateMeta( MLLMTemplateType.mimo_vl, template_cls=Qwen2_5VLTemplate, default_system='You are MiMo, an AI assistant developed by Xiaomi.')) class Qwen3VLTemplate(Qwen2VLTemplate): version = 'v3' def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = Template._encode(self, inputs) processor = self.processor input_ids = encoded['input_ids'] labels = encoded['labels'] loss_scale = encoded.get('loss_scale', None) mm_mask = [False] * len(input_ids) for media_type in ['images', 'videos']: mm_data = getattr(inputs, media_type) if mm_data: if media_type == 'images': media_token = self.image_token_id media_inputs = processor.image_processor(images=mm_data, return_tensors='pt', do_resize=False) media_grid_thw = media_inputs['image_grid_thw'] else: split_token = self._tokenize('\n')[0] media_inputs = processor( text=['\n'.join(['<|vision_start|><|video_pad|><|vision_end|>'] * len(mm_data))], videos=mm_data, return_tensors='pt', do_resize=False, **inputs.mm_processor_kwargs) splited_tokens = self._split_list(media_inputs['input_ids'][0].tolist(), split_token) media_grid_thw = media_inputs['video_grid_thw'] media_inputs.pop('input_ids', None) media_inputs.pop('attention_mask', None) media_token = self.video_token_id idx_list = findall(input_ids, media_token) merge_length = processor.image_processor.merge_size**2 def _get_new_tokens(i): if media_type == 'images': token_len = (media_grid_thw[i].prod() // merge_length) return [media_token] * token_len else: return splited_tokens[i] input_ids, labels, loss_scale, mm_mask = self._extend_tokens( input_ids, labels, loss_scale, idx_list, _get_new_tokens, mm_mask=mm_mask) encoded.update(media_inputs) encoded['input_ids'] = input_ids encoded['labels'] = labels encoded['loss_scale'] = loss_scale if self.requires_mm_token_type_ids and any(mm_mask): encoded['mm_token_type_ids'] = self.create_mm_token_type_ids(input_ids, mm_mask) return encoded def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]: return inputs register_template( QwenTemplateMeta( MLLMTemplateType.qwen3_vl, template_cls=Qwen3VLTemplate, default_system=None, thinking_prefix='\n')) class Qwen3_5Template(Qwen3VLTemplate): image_token_id = 248056 video_token_id = 248057 def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]: if self.padding_free and self.sequence_parallel_size <= 1 and not self.transformers_5_9: raise RuntimeError('Qwen3.5 packing/padding_free with sequence_parallel_size=1 requires ' f'transformers>=5.9.0 (current: {self.transformers_version}). ') return Qwen2VLTemplate._post_encode(self, model, inputs) def _swift_prepare_inputs(self, inputs: StdTemplateInputs): # Normalize message content so the swift backend byte-matches Qwen3.5/Qwen3.6 # HF `chat_template.jinja` rendering (per-role `|trim` and canonical padding). # Must run BEFORE super(), because super() merges/wraps tool messages into # `...` blobs using the raw inner content. # See: https://github.com/modelscope/ms-swift/issues/9276 if isinstance(inputs.system, str): inputs.system = inputs.system.strip() for message in inputs.messages: role = message.get('role') content = message.get('content') if not isinstance(content, str): continue if role in ('user', 'system', 'tool'): # HF applies `|trim` to user/system/tool content. message['content'] = content.strip() elif role == 'assistant': # HF applies `|trim` and re-wraps the ... block with canonical newlines. stripped = content.strip() if '' in stripped and '' in stripped: before, _, after = stripped.partition('') reasoning = before.rstrip('\n').rsplit('', 1)[-1].lstrip('\n').strip() rest = after.lstrip('\n') message['content'] = f'\n{reasoning}\n\n\n{rest}' else: message['content'] = stripped super()._swift_prepare_inputs(inputs) register_template( QwenTemplateMeta( MLLMTemplateType.qwen3_5, template_cls=Qwen3_5Template, default_system=None, thinking_prefix='\n', non_thinking_prefix='\n\n\n\n', agent_template='qwen3_5', is_thinking=True)) class Qwen3VLEmbTemplate(Qwen3VLTemplate): def _preprocess_inputs(self, inputs: StdTemplateInputs) -> None: super()._preprocess_inputs(inputs) if len(inputs.messages) % 2 == 1 and inputs.messages[-1]['role'] != 'assistant': inputs.messages.append({'role': 'assistant', 'content': ''}) register_template( QwenTemplateMeta( MLLMTemplateType.qwen3_vl_emb, default_system="Represent the user's input.", suffix=['<|endoftext|>'], template_cls=Qwen3VLEmbTemplate, )) class Qwen3VLRerankerTemplate(Qwen3VLTemplate): instruction = 'Given a search query, retrieve relevant candidates that answer the query.' def _preprocess_inputs(self, inputs: StdTemplateInputs) -> None: super()._preprocess_inputs(inputs) if inputs.system is not None: instruction = inputs.system inputs.system = None else: instruction = self.instruction query = inputs.messages[0]['content'] document = inputs.messages[1]['content'] user_message = ': ' + instruction + ':' + query + '\n' + ':' + document inputs.messages = [{'role': 'user', 'content': user_message}] return inputs register_template( QwenTemplateMeta( MLLMTemplateType.qwen3_vl_reranker, default_system=qwen3_reranker_system, template_cls=Qwen3VLRerankerTemplate)) # ref: trim to hop multiple so WhisperFeatureExtractor matches native HF (floor frames); # vLLM pad_to_hop_length becomes no-op on pre-trimmed waveforms (GRPO train/rollout align). def trim_audio_to_hop_length(x: np.ndarray, hop_length: int) -> np.ndarray: length = x.shape[-1] aligned = (length // hop_length) * hop_length if 0 < aligned < length: x = x[..., :aligned] return x class Qwen2_5OmniTemplate(Qwen2_5VLTemplate): version = 'omni_v2_5' placeholder_tokens = ['<|IMAGE|>', '<|AUDIO|>', '<|VIDEO|>'] _requires_mm_token_type_ids = False def init_processor(self, processor) -> None: if processor is None: return super().init_processor(processor) if self.version == 'omni_v2_5': from transformers.models.qwen2_5_omni.processing_qwen2_5_omni import Qwen2_5OmniProcessorKwargs default = Qwen2_5OmniProcessorKwargs._defaults elif self.version == 'omni_v3': from transformers.models.qwen3_omni_moe.processing_qwen3_omni_moe import Qwen3OmniMoeProcessorKwargs default = Qwen3OmniMoeProcessorKwargs._defaults # Fix: WhisperFeatureExtractor defaults to truncation=True, which silently # truncates audio longer than 30s. Qwen3 Omni supports variable-length audio, # so we must disable truncation. See: huggingface/transformers#41473 default.setdefault('audio_kwargs', {}) default['audio_kwargs']['truncation'] = False self.seconds_per_chunk = default['videos_kwargs']['seconds_per_chunk'] self.position_id_per_seconds = default['videos_kwargs']['position_id_per_seconds'] self.use_audio_in_video = get_env_args('use_audio_in_video', bool, False) self.sampling_rate = get_env_args('sampling_rate', int, self.processor.feature_extractor.sampling_rate) def _trim_omni_v3_audios(self, audios): """Trim waveforms to hop-length multiple (omni_v3 only). Matches native HF floor framing.""" if self.version != 'omni_v3' or not audios: return audios hop = self.processor.feature_extractor.hop_length trimmed = [] for audio in audios: if isinstance(audio, tuple): # train: (wav, 'video'); vllm standalone: (wav, sr) trimmed.append((trim_audio_to_hop_length(audio[0], hop), audio[1])) elif isinstance(audio, np.ndarray): trimmed.append(trim_audio_to_hop_length(audio, hop)) else: raise TypeError(f'unexpected audio type {type(audio)!r}; expected ndarray or (ndarray, meta)') return trimmed def _encode_truncated(self, inputs: StdTemplateInputs): encoded = super()._encode_truncated(inputs) if self.mode == 'vllm' and inputs.audios: inputs.audios = self._trim_omni_v3_audios(inputs.audios) if 'audios' in encoded: encoded['audios'] = inputs.audios return encoded def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: from qwen_omni_utils import fetch_image, fetch_video kwargs = {'image_patch_size': self.processor.image_processor.patch_size} if self.version == 'omni_v3' else {} sampling_rate = inputs.chat_template_kwargs.get('sampling_rate') if sampling_rate is None: sampling_rate = self.sampling_rate if self.mode == 'vllm': # https://github.com/modelscope/ms-swift/issues/8445 inputs.mm_processor_kwargs['do_resize'] = False if media_type == 'image': inputs.images[index] = fetch_image({'image': inputs.images[index], **inputs.chat_template_kwargs}, **kwargs) if self.version == 'omni_v2_5': return ['<|vision_bos|><|IMAGE|><|vision_eos|>'] elif self.version == 'omni_v3': return ['<|vision_start|><|image_pad|><|vision_end|>'] elif media_type == 'audio': if self.mode != 'vllm': inputs.audios[index] = load_audio(inputs.audios[index], sampling_rate) if self.version == 'omni_v2_5': return ['<|audio_bos|><|AUDIO|><|audio_eos|>'] elif self.version == 'omni_v3': return ['<|audio_start|><|audio_pad|><|audio_end|>'] elif media_type == 'video': video = inputs.videos[index] video_inputs = {'video': video, **inputs.chat_template_kwargs} if isinstance(video, list): # image list from qwen_omni_utils import vision_process video_inputs['sample_fps'] = vision_process.FPS _video = fetch_video(video_inputs, **kwargs) if isinstance(_video, torch.Tensor): _video = _video.to(torch.uint8) inputs.videos[index] = _video if self.use_audio_in_video: if isinstance(video, list): # image list raise ValueError('image list as video input does not support use_audio_in_video') audio = load_audio(video, sampling_rate) if self.mode != 'vllm': inputs.audios.insert(inputs.audio_idx, (audio, 'video')) else: inputs.audios.insert(inputs.audio_idx, audio) inputs.mm_processor_kwargs['use_audio_in_video'] = True inputs.audio_idx += 1 if self.version == 'omni_v2_5': return ['<|vision_bos|><|audio_bos|><|VIDEO|><|audio_eos|><|vision_eos|>'] elif self.version == 'omni_v3': if self.mode == 'vllm': return ['<|vision_start|><|video_pad|><|vision_end|>'] else: return ['<|vision_start|><|audio_start|><|video_pad|><|audio_end|><|vision_end|>'] if self.version == 'omni_v2_5': return ['<|vision_bos|><|VIDEO|><|vision_eos|>'] elif self.version == 'omni_v3': return ['<|vision_start|><|video_pad|><|vision_end|>'] def _get_feat_extract_output_lengths(self, input_lengths): if self.version == 'omni_v2_5': return ((input_lengths - 1) // 2 + 1 - 2) // 2 + 1 elif self.version == 'omni_v3': input_lengths_leave = input_lengths % 100 feat_lengths = (input_lengths_leave - 1) // 2 + 1 return ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13 def _get_new_tokens_use_audio_in_video(self, i, *, video_grid_thw, video_second_per_grid, audio_lengths, video_token_id, audio_token_id): merge_size = self.processor.image_processor.merge_size grid_thw = video_grid_thw[i] height = grid_thw[1] // merge_size width = grid_thw[2] // merge_size audio_token_indices = torch.arange(audio_lengths[i]) video_token_indices = torch.arange(grid_thw[0]).reshape(-1, 1, 1) video_token_indices = torch.broadcast_to(video_token_indices, (video_token_indices.shape[0], height, width)).reshape(-1) video_token_indices = (video_token_indices * video_second_per_grid[i] * self.position_id_per_seconds) if self.version == 'omni_v2_5': tokens_per_chunk = int(self.position_id_per_seconds * self.seconds_per_chunk) video_chunk_indexes = self.processor.get_chunked_index(video_token_indices, tokens_per_chunk) audio_chunk_indexes = self.processor.get_chunked_index(audio_token_indices, tokens_per_chunk) res = [] for j in range(max(len(video_chunk_indexes), len(audio_chunk_indexes))): if j < len(video_chunk_indexes): video_seq_length = video_chunk_indexes[j][1] - video_chunk_indexes[j][0] res += video_token_id * video_seq_length if j < len(audio_chunk_indexes): audio_seq_length = audio_chunk_indexes[j][1] - audio_chunk_indexes[j][0] res += audio_token_id * audio_seq_length return res elif self.version == 'omni_v3': res = [] video_data_index, audio_data_index = 0, 0 while video_data_index < len(video_token_indices) and audio_data_index < len(audio_token_indices): if video_token_indices[video_data_index] <= audio_token_indices[audio_data_index]: res += video_token_id video_data_index += 1 else: res += audio_token_id audio_data_index += 1 if video_data_index < len(video_token_indices): res += video_token_id * (len(video_token_indices) - video_data_index) if audio_data_index < len(audio_token_indices): res += audio_token_id * (len(audio_token_indices) - audio_data_index) return res def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = Template._encode(self, inputs) inputs.audios = self._trim_omni_v3_audios(inputs.audios) processor = self.processor video_audios_mask = [] for i, audio in enumerate(inputs.audios): if isinstance(audio, tuple) and audio[1] == 'video': inputs.audios[i] = audio[0] video_audios_mask.append(True) else: video_audios_mask.append(False) video_audios_mask = torch.tensor(video_audios_mask) do_resize = self.version == 'omni_v3' media_inputs = processor( text='', audio=inputs.audios or None, images=inputs.images or None, videos=inputs.videos or None, do_resize=do_resize, return_tensors='pt') media_inputs.pop('input_ids') media_inputs.pop('attention_mask') media_inputs = to_float_dtype(media_inputs, self.model_info.torch_dtype) input_ids = encoded['input_ids'] labels = encoded['labels'] loss_scale = encoded.get('loss_scale', None) config = self.config.thinker_config # audio if self.version == 'omni_v3': audio_token_id = [config.audio_token_id] else: audio_token_id = self._tokenize('<|AUDIO|>') idx_list = findall(input_ids, audio_token_id) feature_attention_mask = media_inputs.get('feature_attention_mask') if feature_attention_mask is not None: audio_feature_lengths = torch.sum(feature_attention_mask, dim=1) audio_lengths = self._get_feat_extract_output_lengths(audio_feature_lengths) else: audio_lengths = None audio_lengths_origin = audio_lengths if idx_list: if self.use_audio_in_video: audio_lengths = audio_lengths[~video_audios_mask] def _get_new_audio_tokens(i): return audio_token_id * audio_lengths[i] input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_audio_tokens) for media_type in ['image', 'video']: if self.version == 'omni_v3': token_id = [getattr(config, f'{media_type}_token_id')] else: token = f'<|{media_type.upper()}|>' token_id = self._tokenize(token) idx_list = findall(input_ids, token_id) if idx_list: merge_size = processor.image_processor.merge_size media_grid_thw = media_inputs.get(f'{media_type}_grid_thw') if media_type == 'video' and self.use_audio_in_video: audio_lengths = audio_lengths_origin[video_audios_mask] video_second_per_grid = media_inputs['video_second_per_grid'] _get_new_tokens_use_audio_in_video = partial( self._get_new_tokens_use_audio_in_video, video_grid_thw=media_grid_thw, video_second_per_grid=video_second_per_grid, audio_lengths=audio_lengths, video_token_id=token_id, audio_token_id=audio_token_id) input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_tokens_use_audio_in_video) else: def _get_new_tokens(i): token_len = (media_grid_thw[i].prod() // (merge_size**2)) return token_id * token_len input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_tokens) encoded['input_ids'] = input_ids encoded['labels'] = labels encoded['loss_scale'] = loss_scale encoded.update(media_inputs) return encoded def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]: if not self.is_training: return inputs input_ids = inputs['input_ids'] input_features = inputs.get('input_features') feature_attention_mask = inputs.get('feature_attention_mask') base_model = self.get_base_model(model) inputs_embeds = base_model.thinker.model.embed_tokens(input_ids) thinker_config = model.config.thinker_config inputs_embeds = self._get_inputs_embeds_hf(inputs_embeds, inputs, model.thinker.visual, self.processor, thinker_config) if input_features is None: if is_deepspeed_enabled() and not is_deepspeed_zero3_enabled(): # Note: ZeRO-3 still results in hangs; for audio training, please use ZeRO-2. input_features = input_ids.new_zeros([1, 128, 128], dtype=model.thinker.audio_tower.dtype) feature_attention_mask = input_ids.new_ones([1, 128], dtype=torch.bool) audio_res = model.thinker.get_audio_features(input_features, feature_attention_mask) if hasattr(audio_res, 'last_hidden_state'): audio_embeds = audio_res.last_hidden_state else: audio_embeds = audio_res inputs_embeds = inputs_embeds + audio_embeds.mean() * 0. else: audio_res = model.thinker.get_audio_features(input_features, feature_attention_mask) if hasattr(audio_res, 'last_hidden_state'): audio_embeds = audio_res.last_hidden_state else: audio_embeds = audio_res audio_mask = (input_ids == thinker_config.audio_token_index).unsqueeze(-1).expand_as(inputs_embeds) audio_embeds = audio_embeds.to(inputs_embeds.device, inputs_embeds.dtype) inputs_embeds = inputs_embeds.masked_scatter(audio_mask, audio_embeds) return {'inputs_embeds': inputs_embeds} def _get_get_rope_index(self): return self._get_model().thinker.get_rope_index def _get_position_ids(self, inputs: Dict[str, Any]): if not self.is_training: return {} feature_attention_mask = inputs.get('feature_attention_mask') if feature_attention_mask is not None: audio_feature_lengths = torch.sum(feature_attention_mask, dim=1) else: audio_feature_lengths = None video_second_per_grid = inputs.pop('video_second_per_grid', None) input_ids = inputs['input_ids'] attention_mask = inputs.get('attention_mask_2d') if attention_mask is None: attention_mask = inputs.get('attention_mask') if attention_mask is None: attention_mask = torch.ones_like(input_ids) position_ids, _ = self._get_get_rope_index()( input_ids, inputs.get('image_grid_thw'), inputs.get('video_grid_thw'), attention_mask, self.use_audio_in_video, audio_feature_lengths, video_second_per_grid, ) if torch.is_floating_point(position_ids): position_ids = position_ids.to(torch.int64) return {'position_ids': self._concat_text_position_ids(position_ids)} def _data_collator_mm_data(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: res = super()._data_collator_mm_data(batch) video_second_per_grid = self.gather_list(batch, 'video_second_per_grid') if video_second_per_grid: res['video_second_per_grid'] = video_second_per_grid input_features = [b['input_features'] for b in batch if b.get('input_features') is not None] feature_attention_mask = [ b['feature_attention_mask'] for b in batch if b.get('feature_attention_mask') is not None ] if input_features: if self.version == 'omni_v3': max_length = max(input_feature.shape[-1] for input_feature in input_features) for i, input_feature in enumerate(input_features): mask = feature_attention_mask[i] input_features[i] = F.pad(input_feature, (0, max_length - input_feature.shape[-1])) feature_attention_mask[i] = F.pad(mask, (0, max_length - mask.shape[-1])) res['input_features'] = torch.concat(input_features) res['feature_attention_mask'] = torch.concat(feature_attention_mask) return res def generate(self, model, *args, **kwargs): if kwargs.get('video_grid_thw') is not None: kwargs['use_audio_in_video'] = self.use_audio_in_video return super().generate(model, *args, **kwargs) register_template(QwenTemplateMeta(MLLMTemplateType.qwen2_5_omni, template_cls=Qwen2_5OmniTemplate)) class Qwen3OmniTemplate(Qwen2_5OmniTemplate): version = 'omni_v3' norm_bbox = 'norm1000' placeholder_tokens = ['<|image_pad|>', '<|audio_pad|>', '<|video_pad|>'] def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]: return inputs register_template( QwenTemplateMeta( MLLMTemplateType.qwen3_omni, template_cls=Qwen3OmniTemplate, default_system=None, thinking_prefix='\n')) def _qwen3_asr_get_feat_extract_output_lengths(input_lengths): """Qwen3-ASR Conv2d encoder output length: chunks of 100 frames, 13 tokens each.""" input_lengths_leave = input_lengths % 100 feat_lengths = (input_lengths_leave - 1) // 2 + 1 output_lengths = ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13 return output_lengths class Qwen3ASRTemplate(Template): placeholder_tokens = ['<|audio_pad|>'] support_padding_free = True def init_env_args(self) -> None: super().init_env_args() self.sampling_rate = get_env_args('sampling_rate', int, self.processor.feature_extractor.sampling_rate) def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: assert media_type == 'audio' return ['<|audio_start|><|audio_pad|><|audio_end|>'] def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = super()._encode(inputs) sampling_rate = inputs.chat_template_kwargs.get('sampling_rate') if sampling_rate is None: sampling_rate = self.sampling_rate if inputs.audios: audios = load_batch(inputs.audios, load_func=partial(load_audio, sampling_rate=sampling_rate)) audio_inputs = self.processor.feature_extractor( audios, sampling_rate=sampling_rate, return_attention_mask=True, return_tensors='pt', padding=True, truncation=False) audio_inputs['feature_attention_mask'] = audio_inputs.pop('attention_mask') audio_inputs['input_features'] = to_float_dtype(audio_inputs['input_features'], self.model_info.torch_dtype) encoded.update(audio_inputs) input_ids = encoded['input_ids'] labels = encoded['labels'] loss_scale = encoded.get('loss_scale') audio_token_id = self._tokenize('<|audio_pad|>') idx_list = findall(input_ids, audio_token_id) if idx_list: feature_attention_mask = audio_inputs.get('feature_attention_mask') if feature_attention_mask is not None: audio_feature_lengths = torch.sum(feature_attention_mask, dim=1) audio_lengths = _qwen3_asr_get_feat_extract_output_lengths(audio_feature_lengths) def _get_new_audio_tokens(i): return audio_token_id * int(audio_lengths[i]) input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list, _get_new_audio_tokens) encoded['input_ids'] = input_ids encoded['labels'] = labels encoded['loss_scale'] = loss_scale return encoded def _data_collator_mm_data(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: res = super()._data_collator_mm_data(batch) input_features = [b['input_features'] for b in batch if b.get('input_features') is not None] feature_attention_mask = [ b['feature_attention_mask'] for b in batch if b.get('feature_attention_mask') is not None ] if input_features: max_length = max(input_feature.shape[-1] for input_feature in input_features) for i, input_feature in enumerate(input_features): mask = feature_attention_mask[i] input_features[i] = F.pad(input_feature, (0, max_length - input_feature.shape[-1])) feature_attention_mask[i] = F.pad(mask, (0, max_length - mask.shape[-1])) res['input_features'] = torch.concat(input_features) res['feature_attention_mask'] = torch.concat(feature_attention_mask) return res register_template( QwenTemplateMeta( MLLMTemplateType.qwen3_asr, template_cls=Qwen3ASRTemplate, # Even without adding a system message, # the '<|im_start|>system\n<|im_end|>\n' prefix is still present. # Align with the qwen3_asr template. system_prefix=None, default_system=None, prefix=['<|im_start|>system\n{{SYSTEM}}<|im_end|>\n'])) class Qwen3TTSTemplate(Template): # ref: https://github.com/QwenLM/Qwen3-TTS/tree/main/finetuning support_padding_free = False use_model = True model_accepts_loss_kwargs = False def init_env_args(self) -> None: super().init_env_args() self._config_initialized = False self.target_speaker_embedding = None # Cache TTS config values for data collation config = self.config self._tts_pad_token_id = config.tts_pad_token_id self._tts_bos_token_id = config.tts_bos_token_id self._tts_eos_token_id = config.tts_eos_token_id talker_config = config.talker_config self._codec_nothink_id = talker_config.codec_nothink_id self._codec_think_bos_id = talker_config.codec_think_bos_id self._codec_think_eos_id = talker_config.codec_think_eos_id self._codec_pad_id = talker_config.codec_pad_id self._codec_bos_id = talker_config.codec_bos_id self._codec_eos_token_id = talker_config.codec_eos_token_id @staticmethod def _extract_ref_mel(ref_audio_path: str) -> torch.Tensor: """Extract mel spectrogram from reference audio for speaker embedding.""" import librosa from qwen_tts.core.models.modeling_qwen3_tts import mel_spectrogram audio, sr = librosa.load(ref_audio_path, sr=None, mono=True) if audio.ndim > 1: audio = np.mean(audio, axis=-1) if sr != 24000: audio = librosa.resample(audio, orig_sr=sr, target_sr=24000) mels = mel_spectrogram( torch.from_numpy(audio.astype(np.float32)).unsqueeze(0), n_fft=1024, num_mels=128, sampling_rate=24000, hop_size=256, win_size=1024, fmin=0, fmax=12000).transpose(1, 2) # [1, mel_len, 128] return mels def _preprocess_inputs(self, inputs: StdTemplateInputs) -> None: """Override to skip _add_default_tags since audios here are targets, not inputs.""" pass def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: # Get text from messages (assistant content) text = inputs.messages[-1]['content'] if inputs.messages else '' # Build TTS text with assistant markers tts_text = f'<|im_start|>assistant\n{text}' text_ids = self._tokenize(tts_text) # Get audio codes (pre-extracted or online) audio_codes = inputs.extra_kwargs.get('audio_codes') if audio_codes is None: audio_path = inputs.audios[0] if inputs.audios else None if audio_path: tts_tokenizer = self.processor.tts_tokenizer enc_res = tts_tokenizer.encode([audio_path]) audio_codes = enc_res.audio_codes[0].cpu().tolist() assert audio_codes is not None, "Either 'audio_codes' or 'audio'/'audios' must be provided in the dataset." audio_codes = torch.tensor(audio_codes, dtype=torch.long) # [t, 16] # Extract mel spectrogram from reference audio ref_audios = inputs.extra_kwargs.get('ref_audios') ref_audio_path = ref_audios[0] assert ref_audio_path is not None, "'ref_audios' must be provided in the dataset." ref_mel = self._extract_ref_mel(ref_audio_path) # [1, mel_len, 128] return { 'input_ids': text_ids, # dummy for length tracking 'labels': None, 'tts_audio_codes': audio_codes, # [codec_len, 16] 'tts_ref_mel': ref_mel, # [1, mel_len, 128] } def compute_sft_loss(self, model, inputs, num_items_in_batch=None, trainer=None): """Override to bypass standard label adjustment - TTS loss is computed in forward. Combines the talker codec_0 cross-entropy loss with the sub-talker loss using a fixed weighting factor of 0.3. """ # Extract speaker_embedding from ref_mels and cache for checkpoint post-processing if 'ref_mels' in inputs: base_model = model.module if hasattr(model, 'module') else model with torch.no_grad(): speaker_embedding = base_model.speaker_encoder(inputs['ref_mels'].to(base_model.device).to( base_model.dtype)).detach() if self.target_speaker_embedding is None: self.target_speaker_embedding = speaker_embedding.cpu() inputs.pop('ref_mels') inputs['speaker_embedding'] = speaker_embedding outputs = model(**inputs) logits = outputs.logits shift_labels = inputs['codec_0_labels'][:, 1:].contiguous() talker_loss = F.cross_entropy( logits.reshape(-1, logits.shape[-1]).float(), shift_labels.reshape(-1).to(logits.device), ignore_index=-100, ) sub_talker_loss = getattr(outputs, 'sub_talker_loss', None) if sub_talker_loss is not None: outputs['loss'] = talker_loss + 0.3 * sub_talker_loss else: outputs['loss'] = talker_loss return outputs def save_callback(self, model, output_dir): """Custom save: drop speaker_encoder weights and inject target_speaker_embedding into codec_embedding.weight[3000].""" shutil.copytree(model.config.name_or_path, output_dir, dirs_exist_ok=True) with open(os.path.join(model.config.name_or_path, 'config.json'), 'r', encoding='utf-8') as f: config_dict = json.load(f) speaker_name = get_env_args('speaker_name', str, 'speaker_test') config_dict['tts_model_type'] = 'custom_voice' config_dict['talker_config']['spk_id'] = {speaker_name: 3000} config_dict['talker_config']['spk_is_dialect'] = {speaker_name: False} from safetensors.torch import save_file from transformers.modeling_utils import unwrap_model base_model = unwrap_model(model) state_dict = {k: v.detach().cpu() for k, v in base_model.state_dict().items()} # 1. Drop speaker_encoder keys keys_to_drop = [k for k in state_dict if k.startswith('speaker_encoder')] for k in keys_to_drop: del state_dict[k] # 2. Inject target_speaker_embedding into codec_embedding.weight[3000] emb_key = 'talker.model.codec_embedding.weight' if self.target_speaker_embedding is not None and emb_key in state_dict: weight = state_dict[emb_key] state_dict[emb_key][3000] = self.target_speaker_embedding[0].to(weight.dtype) save_file(state_dict, os.path.join(output_dir, 'model.safetensors')) # Save config with open(os.path.join(output_dir, 'config.json'), 'w', encoding='utf-8') as f: json.dump(config_dict, f, indent=2, ensure_ascii=False) def data_collator(self, batch: List[Dict[str, Any]], *, padding_to=None) -> Dict[str, Any]: """Custom TTS data collation - builds dual-channel input format.""" item_length = [len(b['input_ids']) + b['tts_audio_codes'].shape[0] for b in batch] max_length = max(item_length) + 8 b_size, t = len(batch), max_length input_ids = torch.zeros((b_size, t, 2), dtype=torch.long) codec_ids = torch.zeros((b_size, t, 16), dtype=torch.long) text_embedding_mask = torch.zeros((b_size, t), dtype=torch.bool) codec_embedding_mask = torch.zeros((b_size, t), dtype=torch.bool) codec_mask = torch.zeros((b_size, t), dtype=torch.bool) attention_mask = torch.zeros((b_size, t), dtype=torch.long) codec_0_labels = torch.full((b_size, t), -100, dtype=torch.long) for i, data in enumerate(batch): text_ids = torch.tensor(data['input_ids']) # [text_len] audio_codes = data['tts_audio_codes'] # [codec_len, 16] audio_codec_0 = audio_codes[:, 0] text_ids_len = len(text_ids) codec_ids_len = audio_codec_0.shape[0] # === Text channel === input_ids[i, :3, 0] = text_ids[:3] input_ids[i, 3:7, 0] = self._tts_pad_token_id input_ids[i, 7, 0] = self._tts_bos_token_id input_ids[i, 8:8 + text_ids_len - 3, 0] = text_ids[3:] input_ids[i, 8 + text_ids_len - 3, 0] = self._tts_eos_token_id input_ids[i, 8 + text_ids_len - 2:8 + text_ids_len + codec_ids_len, 0] = self._tts_pad_token_id text_embedding_mask[i, :8 + text_ids_len + codec_ids_len] = True # === Codec channel === input_ids[i, 3:8, 1] = torch.tensor([ self._codec_nothink_id, self._codec_think_bos_id, self._codec_think_eos_id, 0, # placeholder for speaker embedding self._codec_pad_id, ]) input_ids[i, 8:8 + text_ids_len - 3, 1] = self._codec_pad_id input_ids[i, 8 + text_ids_len - 3, 1] = self._codec_pad_id input_ids[i, 8 + text_ids_len - 2, 1] = self._codec_bos_id input_ids[i, 8 + text_ids_len - 1:8 + text_ids_len - 1 + codec_ids_len, 1] = audio_codec_0 input_ids[i, 8 + text_ids_len - 1 + codec_ids_len, 1] = self._codec_eos_token_id # === Labels (codec layer 0) === codec_0_labels[i, 8 + text_ids_len - 1:8 + text_ids_len - 1 + codec_ids_len] = audio_codec_0 codec_0_labels[i, 8 + text_ids_len - 1 + codec_ids_len] = self._codec_eos_token_id # === Sub-talker codec IDs === codec_ids[i, 8 + text_ids_len - 1:8 + text_ids_len - 1 + codec_ids_len, :] = audio_codes # === Masks === codec_embedding_mask[i, 3:8 + text_ids_len + codec_ids_len] = True codec_embedding_mask[i, 6] = False # speaker embedding position codec_mask[i, 8 + text_ids_len - 1:8 + text_ids_len - 1 + codec_ids_len] = True attention_mask[i, :8 + text_ids_len + codec_ids_len] = True ref_mels = torch.cat([data['tts_ref_mel'] for data in batch], dim=0) return { 'input_ids': input_ids, 'ref_mels': ref_mels, 'attention_mask': attention_mask, 'text_embedding_mask': text_embedding_mask.unsqueeze(-1), 'codec_embedding_mask': codec_embedding_mask.unsqueeze(-1), 'labels': codec_0_labels, 'codec_0_labels': codec_0_labels, 'codec_ids': codec_ids, 'codec_mask': codec_mask, } register_template(QwenTemplateMeta( MLLMTemplateType.qwen3_tts, template_cls=Qwen3TTSTemplate, default_system=None, )) class Ovis1_6Template(Template): skip_prompt = False use_model = True def init_env_args(self): super().init_env_args() self.max_partition = get_env_args('max_partition', int, 9) def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: assert media_type == 'image' return [[-200], '\n'] def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]: encoded = super()._encode(inputs) images = inputs.images input_ids = encoded['input_ids'] labels = encoded['labels'] idx_list = findall(input_ids, [-200]) added_tokens_len = 0 pixel_values = [] for i, idx in enumerate(idx_list): raw_pixel_values, image_placeholders = self.model.visual_tokenizer.preprocess_image( images[i], max_partition=self.max_partition) input_ids = input_ids[:idx] + image_placeholders + input_ids[idx + 1:] if labels is not None: labels = labels[:idx] + [-100] * len(image_placeholders) + labels[idx + 1:] pixel_values.append(raw_pixel_values) added_tokens_len += len(image_placeholders) - 1 dtype = self.model.visual_tokenizer.dtype if pixel_values: pixel_values = torch.cat(pixel_values, dim=0).to(dtype) else: pixel_values = torch.zeros((1, 3, 384, 384), dtype=dtype) # dummpy encoded.update({'input_ids': input_ids, 'labels': labels}) encoded['pixel_values'] = [pixel_values] return encoded def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]: padding_side = self.padding_side if self.is_training else 'left' if self.max_length is not None: model.config.multimodal_max_length = self.max_length input_ids = inputs['input_ids'] labels = inputs.get('labels') if labels is None: labels = input_ids.new_full(input_ids.shape, -100) _, inputs_embeds, labels, attention_mask = model.merge_multimodal( text_input_ids=input_ids, text_attention_masks=torch.ones_like(input_ids), # not use, only compat text_labels=labels, pixel_values=inputs['pixel_values'], left_padding=padding_side == 'left') if inputs.get('labels') is None: labels = None return {'inputs_embeds': inputs_embeds, 'labels': labels, 'attention_mask': attention_mask} def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]: pixel_values = self.gather_list(batch, 'pixel_values') res = super()._data_collator(batch, padding_to=padding_to) res['pixel_values'] = pixel_values return res register_template( TemplateMeta( MLLMTemplateType.ovis1_6, prefix=[''], prompt=['user\n{{QUERY}}\nmodel\n'], chat_sep=['\n'], suffix=[''], system_prefix=['system\n{{SYSTEM}}\n'], template_cls=Ovis1_6Template, )) register_template( Llama3TemplateMeta( MLLMTemplateType.ovis1_6_llama3, default_system='You are a helpful and honest multimodal assistant.', template_cls=Ovis1_6Template, agent_template=None, )) class Ovis2Template(Ovis1_6Template): placeholder_tokens = ['<|image_pad|>', '<|video_pad|>'] NFRAMES = 12 def init_env_args(self): super().init_env_args() self.nframes = get_env_args('nframes', int, self.NFRAMES) def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: if media_type == 'image': if self.mode == 'vllm': return ['\n'] return [[-200], '\n'] elif media_type == 'video': inputs.images = load_video_ovis2(inputs.videos[index], self.nframes) return [[-200] * self.nframes, '\n'] register_template(QwenTemplateMeta( MLLMTemplateType.ovis2, template_cls=Ovis2Template, )) class Ovis2_5Template(Template): use_model = True skip_prompt = False support_padding_free = True def init_env_args(self) -> None: super().init_env_args() self.min_pixels = get_env_args('min_pixels', int, 448 * 448) self.max_pixels = get_env_args('max_pixels', int, 1344 * 1792) self.video_max_pixels = get_env_args('video_max_pixels', int, 896 * 896) self.num_frames = get_env_args('num_frames', int, 8) def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, inputs: StdTemplateInputs) -> List[Context]: if media_type == 'image': if self.mode == 'vllm': return [''] else: return [[-200], '\n'] elif media_type == 'video': if self.mode == 'vllm': return ['