From 5eff2f8b94f0ca07bad5f7fa2649749eaa06f860 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Wed, 6 May 2026 20:40:53 +0800 Subject: [PATCH 01/13] support chat_template_kwargs --- swift/dataset/preprocessor/core.py | 1 + swift/template/template_inputs.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/swift/dataset/preprocessor/core.py b/swift/dataset/preprocessor/core.py index 7f8d0b83d6..3d0df336de 100644 --- a/swift/dataset/preprocessor/core.py +++ b/swift/dataset/preprocessor/core.py @@ -34,6 +34,7 @@ class RowPreprocessor: 'channel', 'margin', 'teacher_prompt', + 'chat_template_kwargs', ] def __init__(self, diff --git a/swift/template/template_inputs.py b/swift/template/template_inputs.py index 8165f1d7b1..3108b8019b 100644 --- a/swift/template/template_inputs.py +++ b/swift/template/template_inputs.py @@ -28,9 +28,11 @@ class StdTemplateInputs: objects: Dict[str, Any] = field(default_factory=dict) margin: Optional[float] = None # for reward modeling - mm_processor_kwargs: Dict[str, Any] = field(default_factory=dict) + chat_template_kwargs: Dict[str, Any] = field(default_factory=dict) # from dataset extra_kwargs: Dict[str, Any] = field(default_factory=dict) + mm_processor_kwargs: Dict[str, Any] = field(default_factory=dict) + def __post_init__(self): self.image_idx = 0 self.audio_idx = 0 From 179a5f45e9b17facd86ae314b791691243098715 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Thu, 7 May 2026 00:07:42 +0800 Subject: [PATCH 02/13] update --- swift/dataset/preprocessor/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/swift/dataset/preprocessor/core.py b/swift/dataset/preprocessor/core.py index 3d0df336de..81f5c09363 100644 --- a/swift/dataset/preprocessor/core.py +++ b/swift/dataset/preprocessor/core.py @@ -262,6 +262,7 @@ def _new_init(self, schema=None, features=None, *args, **kwargs): features[key] = messages_feature features['images'] = List({'bytes': Value(dtype='binary'), 'path': Value(dtype='string')}) features['objects'] = Json() + features['chat_template_kwargs'] = Json() else: messages_feature = [{ 'role': Value(dtype='string'), From 59dbc218f783cda8953920428e8f1c5c4c21af23 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Thu, 7 May 2026 00:27:11 +0800 Subject: [PATCH 03/13] update --- docs/source/Customization/Custom-dataset.md | 7 +++++++ docs/source_en/Customization/Custom-dataset.md | 9 +++++++++ requirements/framework.txt | 2 +- swift/template/template_inputs.py | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/source/Customization/Custom-dataset.md b/docs/source/Customization/Custom-dataset.md index f71175a993..cb12dd3f96 100644 --- a/docs/source/Customization/Custom-dataset.md +++ b/docs/source/Customization/Custom-dataset.md @@ -94,6 +94,13 @@ print(inputs['loss_scale']) {"messages": [..., {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"北京\"}}", "loss": false}, {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"上海\"}}"}, ...]} ``` +- "chat_template_kwargs"字段,(需ms-swift>=4.2.1),你可以通过在数据集中传入该字段**样本级别**控制template的min_pixels, max_pixels, fps等多模态参数,以及enable_thinking(推理时)等参数。以下为不同模型支持的参数: + - 其中enable_thinking支持所有模型(推理时生效);max_pixels参数支持所有多模态模型。 + - Qwen系列多模态模型:min_pixels, max_pixels, fps等qwen_vl_utils/qwen_omni_utils支持的参数。 +```jsonl +{"messages": [{"role": "user", "content": "这是什么"}, {"role": "assistant", "content": "这是一只兔子", "loss": false}], "chat_template_kwargs": {"max_pixels": 1048576}} +{"messages": [{"role": "user", "content": "who are you?"}], "chat_template_kwargs": {"enable_thinking": false}} +``` #### channel loss 如果你要使用channel loss,你需要设置`--enable_channel_loss true`,并在数据集中增加"channel"字段。channel loss兼容packing/padding_free/loss_scale等技术。 diff --git a/docs/source_en/Customization/Custom-dataset.md b/docs/source_en/Customization/Custom-dataset.md index 3b99737883..9d88f3d581 100644 --- a/docs/source_en/Customization/Custom-dataset.md +++ b/docs/source_en/Customization/Custom-dataset.md @@ -96,6 +96,15 @@ Note: If you set "loss"/"loss_scale" on consecutive "tool_call" messages in the {"messages": [..., {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"Beijing\"}}", "loss": false}, {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"Shanghai\"}}"}, ...]} ``` +- The "chat_template_kwargs" field (requires ms-swift>=4.2.1) allows you to control template multimodal parameters such as min_pixels, max_pixels, fps, as well as parameters like enable_thinking (during inference) at the **sample level** by passing this field in the dataset. The following parameters are supported by different models: + - Among them, enable_thinking is supported by all models (takes effect during inference); the max_pixels parameter is supported by all multimodal models. + - Qwen series multimodal models: parameters supported by qwen_vl_utils/qwen_omni_utils such as min_pixels, max_pixels, fps, etc. + +```jsonl +{"messages": [{"role": "user", "content": "What is this"}, {"role": "assistant", "content": "This is a rabbit", "loss": false}], "chat_template_kwargs": {"max_pixels": 1048576}} +{"messages": [{"role": "user", "content": "who are you?"}], "chat_template_kwargs": {"enable_thinking": false}} +``` + #### Channel Loss If you want to use channel loss, you need to set `--enable_channel_loss true` and add a "channel" field to your dataset. Channel loss is compatible with techniques such as packing, padding-free, and loss scaling. diff --git a/requirements/framework.txt b/requirements/framework.txt index cc6f80636f..66ad03f7f4 100644 --- a/requirements/framework.txt +++ b/requirements/framework.txt @@ -32,7 +32,7 @@ sortedcontainers>=1.5.9 tensorboard tiktoken tqdm -transformers>=4.33,<5.8.0 +transformers>=4.33,<5.9.0 transformers_stream_generator trl>=0.15,<1.0 uvicorn diff --git a/swift/template/template_inputs.py b/swift/template/template_inputs.py index 3108b8019b..3fdc44b9b0 100644 --- a/swift/template/template_inputs.py +++ b/swift/template/template_inputs.py @@ -59,7 +59,7 @@ def is_multimodal(self): def from_dict(cls, inputs: Dict[str, Any]) -> 'StdTemplateInputs': inputs = deepcopy(inputs) kwargs = {} - for key in ['label', 'channel', 'margin', 'rejected_response']: + for key in ['label', 'channel', 'margin', 'rejected_response', 'chat_template_kwargs']: if key in inputs: kwargs[key] = inputs[key] messages = inputs['messages'] From 4ee8ee687e772883e628c33ff3231e36ea60aebe Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Thu, 7 May 2026 11:13:35 +0800 Subject: [PATCH 04/13] fix --- swift/template/templates/qwen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/swift/template/templates/qwen.py b/swift/template/templates/qwen.py index de0ac6c80e..00959802bb 100644 --- a/swift/template/templates/qwen.py +++ b/swift/template/templates/qwen.py @@ -316,7 +316,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int # 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]}, **kwargs) + 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: @@ -325,7 +325,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int if self.version == 'v3': kwargs['return_video_metadata'] = True video = inputs.videos[index] - video_inputs = {'video': video} + 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 @@ -652,7 +652,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int # 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]}, **kwargs) + 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': @@ -666,7 +666,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int return ['<|audio_start|><|audio_pad|><|audio_end|>'] elif media_type == 'video': video = inputs.videos[index] - _video = fetch_video({'video': video}, **kwargs) + _video = fetch_video({'video': video, **inputs.chat_template_kwargs}, **kwargs) if isinstance(_video, torch.Tensor): _video = _video.to(torch.uint8) inputs.videos[index] = _video From 44431d660c7a9bf9b46e469ae96b2b32fa2ff828 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Thu, 7 May 2026 13:58:57 +0800 Subject: [PATCH 05/13] fix --- swift/template/base.py | 53 +++++++++++++++++++++----------- swift/template/templates/qwen.py | 6 ++++ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/swift/template/base.py b/swift/template/base.py index fa33de0664..ead36f3e61 100644 --- a/swift/template/base.py +++ b/swift/template/base.py @@ -121,7 +121,7 @@ def __init__( template_meta.default_system = default_system if enable_thinking is None: enable_thinking = template_meta.is_thinking - self._response_prefix = response_prefix + self.response_prefix = response_prefix self.template_meta: 'TemplateMeta' = template_meta self.use_chat_template = use_chat_template self.enable_thinking = enable_thinking @@ -156,13 +156,22 @@ def __init__( if processor is not None: self.init_processor(processor) - @property - def response_prefix(self): - if self._response_prefix is not None: - return self._response_prefix + def _get_enable_thinking(self, inputs): + enable_thinking = inputs.chat_template_kwargs.get('enable_thinking') + if enable_thinking is None: + enable_thinking = self.enable_thinking + return enable_thinking + + def _get_response_prefix(self, inputs): + response_prefix = inputs.chat_template_kwargs.get('response_prefix') + if response_prefix is None: + response_prefix = self.response_prefix + if response_prefix is not None: + return response_prefix elif not self.use_chat_template: return '' - elif self.enable_thinking: + enable_thinking = self._get_enable_thinking(inputs) + if enable_thinking: return self.template_meta.thinking_prefix else: return self.template_meta.non_thinking_prefix @@ -308,6 +317,12 @@ def _preprocess_function_call(self, inputs: StdTemplateInputs) -> None: def prepare_engine_kwargs(self) -> Dict[str, Any]: return {} + def _get_max_pixels(self, inputs): + max_pixels = inputs.chat_template_kwargs.get('max_pixels') + if max_pixels is None: + max_pixels = self.max_pixels + return max_pixels + def _preprocess_inputs( self, inputs: StdTemplateInputs, @@ -320,16 +335,17 @@ def _preprocess_inputs( images = inputs.images load_images = self.load_images or self.mode in {'vllm', 'lmdeploy'} load_images_origin = load_images - if self.max_pixels is not None or inputs.objects: + max_pixels = self._get_max_pixels(inputs) + if max_pixels is not None or inputs.objects: load_images = True if images: for i, image in enumerate(images): images[i] = self._load_image(images[i], load_images) if inputs.objects: self._get_height_width(inputs) - if self.max_pixels is not None: + if max_pixels is not None: # Scale the image proportionally without affecting the scaled objects. - images = [rescale_image(img, self.max_pixels) for img in images] + images = [rescale_image(img, max_pixels) for img in images] if images and not load_images_origin: # fix pt & qwen-vl for i, image in enumerate(images): if isinstance(image, Image.Image): @@ -1066,12 +1082,11 @@ def _jinja_encode(self, inputs: StdTemplateInputs): kwargs = {} if inputs.tools: kwargs['tools'] = inputs.tools - if 'thinking_budget' in inputs.extra_kwargs: - kwargs['thinking_budget'] = inputs.extra_kwargs.get('thinking_budget', 0) - if self.template_meta.is_thinking or self.enable_thinking: - kwargs[self.jinja_enable_thinking_key] = self.enable_thinking - if self.chat_template_kwargs: - kwargs.update(self.chat_template_kwargs) + enable_thinking = self._get_enable_thinking(inputs) + if self.template_meta.is_thinking or enable_thinking: + kwargs[self.jinja_enable_thinking_key] = enable_thinking + kwargs.update(self.chat_template_kwargs) + kwargs.update(inputs.chat_template_kwargs) text = self.tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=add_generation_prompt, **kwargs) answer_len = 1 if self.is_training else 0 @@ -1182,7 +1197,8 @@ def _swift_encode(self, inputs: StdTemplateInputs): if self.use_chat_template: if self.add_non_thinking_prefix: self._add_non_thinking_prefix(inputs) - if template_meta.is_thinking or self.enable_thinking: + enable_thinking = self._get_enable_thinking(inputs) + if template_meta.is_thinking or enable_thinking: self._remove_history_thinking(inputs) system = self._get_system(inputs) else: @@ -1234,6 +1250,7 @@ def _swift_encode(self, inputs: StdTemplateInputs): context_list = prompt.copy() extra_context_list = [] extra_context_type = None + response_prefix = self._get_response_prefix(inputs) if i < n_round - 1: # Not the last round. context_list.append('{{RESPONSE}}') @@ -1264,9 +1281,9 @@ def _swift_encode(self, inputs: StdTemplateInputs): if add_eos: extra_context_list = template_meta.suffix extra_context_type = ContextType.SUFFIX - elif self.response_prefix: + elif response_prefix: # final round and during inference. - context_list.append(self.response_prefix) + context_list.append(response_prefix) self._concat_context_list( context_list, diff --git a/swift/template/templates/qwen.py b/swift/template/templates/qwen.py index f751108974..34ef62c4f4 100644 --- a/swift/template/templates/qwen.py +++ b/swift/template/templates/qwen.py @@ -306,6 +306,9 @@ def init_env_args(self): self.transformers_version = version.parse(transformers.__version__) self.bbox_format = get_env_args('QWENVL_BBOX_FORMAT', str, 'legacy') + def _get_max_pixels(self, inputs): + 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 @@ -672,6 +675,9 @@ def init_processor(self, processor) -> None: 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 _get_max_pixels(self, inputs): + return self.max_pixels + 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 From ffc96d45482d9b904a89e273a3fb7da93ae6ee23 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Thu, 7 May 2026 14:54:14 +0800 Subject: [PATCH 06/13] update --- docs/source/Customization/Custom-dataset.md | 2 +- docs/source_en/Customization/Custom-dataset.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/Customization/Custom-dataset.md b/docs/source/Customization/Custom-dataset.md index cb12dd3f96..ff3a40087d 100644 --- a/docs/source/Customization/Custom-dataset.md +++ b/docs/source/Customization/Custom-dataset.md @@ -95,7 +95,7 @@ print(inputs['loss_scale']) ``` - "chat_template_kwargs"字段,(需ms-swift>=4.2.1),你可以通过在数据集中传入该字段**样本级别**控制template的min_pixels, max_pixels, fps等多模态参数,以及enable_thinking(推理时)等参数。以下为不同模型支持的参数: - - 其中enable_thinking支持所有模型(推理时生效);max_pixels参数支持所有多模态模型。 + - 其中"enable_thinking", "response_prefix"支持所有模型(推理时生效);"max_pixels"参数支持所有多模态模型。 - Qwen系列多模态模型:min_pixels, max_pixels, fps等qwen_vl_utils/qwen_omni_utils支持的参数。 ```jsonl {"messages": [{"role": "user", "content": "这是什么"}, {"role": "assistant", "content": "这是一只兔子", "loss": false}], "chat_template_kwargs": {"max_pixels": 1048576}} diff --git a/docs/source_en/Customization/Custom-dataset.md b/docs/source_en/Customization/Custom-dataset.md index 9d88f3d581..6e5c6f7788 100644 --- a/docs/source_en/Customization/Custom-dataset.md +++ b/docs/source_en/Customization/Custom-dataset.md @@ -97,7 +97,7 @@ Note: If you set "loss"/"loss_scale" on consecutive "tool_call" messages in the ``` - The "chat_template_kwargs" field (requires ms-swift>=4.2.1) allows you to control template multimodal parameters such as min_pixels, max_pixels, fps, as well as parameters like enable_thinking (during inference) at the **sample level** by passing this field in the dataset. The following parameters are supported by different models: - - Among them, enable_thinking is supported by all models (takes effect during inference); the max_pixels parameter is supported by all multimodal models. + - Among them, "enable_thinking" and "response_prefix" are supported by all models (takes effect during inference); the "max_pixels" parameter is supported by all multimodal models. - Qwen series multimodal models: parameters supported by qwen_vl_utils/qwen_omni_utils such as min_pixels, max_pixels, fps, etc. ```jsonl From 2fb5e363f4aaf1ca396369e066f67ccc52234515 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Thu, 7 May 2026 16:04:12 +0800 Subject: [PATCH 07/13] update --- swift/infer_engine/grpo_vllm_engine.py | 2 +- swift/infer_engine/lmdeploy_engine.py | 7 ++++--- swift/infer_engine/protocol.py | 1 + swift/infer_engine/sglang_engine.py | 6 +++--- swift/infer_engine/transformers_engine.py | 8 ++++---- swift/infer_engine/vllm_engine.py | 14 +++++++++---- swift/template/base.py | 25 +++++++++++++++-------- swift/template/template_inputs.py | 4 +++- 8 files changed, 42 insertions(+), 25 deletions(-) diff --git a/swift/infer_engine/grpo_vllm_engine.py b/swift/infer_engine/grpo_vllm_engine.py index 26470720ea..653e0d2de7 100644 --- a/swift/infer_engine/grpo_vllm_engine.py +++ b/swift/infer_engine/grpo_vllm_engine.py @@ -108,7 +108,7 @@ def _create_chat_completion_response(self, result, inputs, request_config, reque choices = [] for output in result.outputs: output.token_ids = list(output.token_ids) - response = self.template.decode(output.token_ids) + response = self.template.decode(output.token_ids, template_inputs=inputs['template_inputs']) logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs) toolcall = self._get_toolcall(response) diff --git a/swift/infer_engine/lmdeploy_engine.py b/swift/infer_engine/lmdeploy_engine.py index a15d67b395..694ff4df6e 100644 --- a/swift/infer_engine/lmdeploy_engine.py +++ b/swift/infer_engine/lmdeploy_engine.py @@ -202,7 +202,7 @@ async def _infer_stream_async( else: context = self.engine.safe_run(session_id) - infer_streamer = InferStreamer(self.template) + infer_streamer = InferStreamer(self.template, template_inputs=inputs['template_inputs']) token_idx = 0 async with context as gen: if version.parse(lmdeploy.__version__) < version.parse('0.6.5'): @@ -225,7 +225,8 @@ async def _infer_stream_async( usage_info = self._get_usage_info(len(inputs['input_ids']), output.num_token) toolcall = None if is_finished: - toolcall = self._get_toolcall(self.template.decode(output.token_ids)) + toolcall = self._get_toolcall( + self.template.decode(output.token_ids, template_inputs=inputs['template_inputs'])) finish_reason = self._get_finish_reason(generation_config.max_new_tokens, output.num_token, output.status.name == 'FINISH') choices = [ @@ -260,7 +261,7 @@ async def _infer_full_async( async for output in generator.async_stream_infer(session_id=session_id, **inputs, **kwargs): pass - response = self.template.decode(output.token_ids) + response = self.template.decode(output.token_ids, template_inputs=inputs['template_inputs']) logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs) usage_info = self._get_usage_info(len(inputs['input_ids']), output.num_token) diff --git a/swift/infer_engine/protocol.py b/swift/infer_engine/protocol.py index c4b5a1a22d..9f1b8cf821 100644 --- a/swift/infer_engine/protocol.py +++ b/swift/infer_engine/protocol.py @@ -91,6 +91,7 @@ class InferRequest: tools: Optional[List[Tool]] = None objects: Dict[str, Any] = field(default_factory=dict) + chat_template_kwargs: Dict[str, Any] = field(default_factory=dict) def __post_init__(self): for key in ['images', 'audios', 'videos']: diff --git a/swift/infer_engine/sglang_engine.py b/swift/infer_engine/sglang_engine.py index e19dd13d41..3d923a3988 100644 --- a/swift/infer_engine/sglang_engine.py +++ b/swift/infer_engine/sglang_engine.py @@ -185,7 +185,7 @@ def _create_chat_completion_response(self, output, inputs, return_details: bool assert output is not None meta_info = output['meta_info'] usage_info = self._get_usage_info(meta_info['prompt_tokens'], meta_info['completion_tokens']) - response = self.template.decode(output['output_ids']) + response = self.template.decode(output['output_ids'], template_inputs=inputs['template_inputs']) toolcall = self._get_toolcall(response) token_ids = output['output_ids'] if return_details else None choice = ChatCompletionResponseChoice( @@ -271,7 +271,7 @@ async def _infer_stream_async(self, inputs: Dict[str, Any], generation_config: D engine_inputs = {k: v for k, v in inputs.items() if k != 'template_inputs'} result_generator = await self.engine.async_generate( **engine_inputs, sampling_params=generation_config, stream=True) - infer_streamer = InferStreamer(self.template) + infer_streamer = InferStreamer(self.template, template_inputs=inputs['template_inputs']) async for output in result_generator: res = self._create_chat_completion_stream_response(output, infer_streamer) if res is None: @@ -289,7 +289,7 @@ def _create_chat_completion_stream_response(self, output, infer_streamer) -> Opt toolcall = None if is_finished: finish_reason = finish_reason['type'] - toolcall = self._get_toolcall(self.template.decode(output['output_ids'])) + toolcall = self._get_toolcall(self.template.decode(output['output_ids'], **infer_streamer.decode_kwargs)) meta_info = output['meta_info'] usage_info = self._get_usage_info(meta_info['prompt_tokens'], meta_info['completion_tokens']) # TODO: logprobs diff --git a/swift/infer_engine/transformers_engine.py b/swift/infer_engine/transformers_engine.py index ac7e963717..8af16deebc 100644 --- a/swift/infer_engine/transformers_engine.py +++ b/swift/infer_engine/transformers_engine.py @@ -217,8 +217,7 @@ def _update_batched_logprobs(batched_logprobs: List[torch.Tensor], logits_stream def _infer_stream(self, inputs: Dict[str, Any], *, generation_config: GenerationConfig, adapter_request: Optional[AdapterRequest], request_config: RequestConfig, - **kwargs) -> Iterator[List[Optional[ChatCompletionStreamResponse]]]: - + template_inputs) -> Iterator[List[Optional[ChatCompletionStreamResponse]]]: if generation_config.num_beams != 1: error_msg = 'Streaming generation does not support beam search.' raise ValueError(error_msg) @@ -248,7 +247,7 @@ def _model_generate(**kwargs): batch_size = inputs['attention_mask'].shape[0] all_is_finished = False is_finished = [False] * batch_size - infer_streamers = [InferStreamer(self.template) for _ in range(batch_size)] + infer_streamers = [InferStreamer(self.template, template_inputs=template_inputs[i]) for i in range(batch_size)] request_id_list = [f'chatcmpl-{random_uuid()}' for _ in range(batch_size)] token_idxs = [0] * batch_size @@ -298,7 +297,8 @@ def _model_generate(**kwargs): usage_info = self._get_usage_info(num_prompt_tokens, len(generate_ids)) toolcall = None if is_finished[i]: - toolcall = self._get_toolcall(self.template.decode(generate_ids)) + toolcall = self._get_toolcall( + self.template.decode(generate_ids, template_inputs=template_inputs[i])) finish_reason = self._get_finish_reason(generation_config.max_new_tokens, usage_info.completion_tokens, is_finished[i]) diff --git a/swift/infer_engine/vllm_engine.py b/swift/infer_engine/vllm_engine.py index 4e5e66c0bb..f1f55ed7a7 100644 --- a/swift/infer_engine/vllm_engine.py +++ b/swift/infer_engine/vllm_engine.py @@ -494,7 +494,9 @@ async def _infer_stream_async( ) -> AsyncIterator[ChatCompletionStreamResponse]: request_id = random_uuid() result_generator = self._add_request(inputs, generation_config, request_id, adapter_request=adapter_request) - infer_streamers = [InferStreamer(self.template) for _ in range(generation_config.n)] + infer_streamers = [ + InferStreamer(self.template, template_inputs=inputs['template_inputs']) for _ in range(generation_config.n) + ] token_idxs = [0 for _ in range(generation_config.n)] async for result in result_generator: res = self._create_chat_completion_stream_response(result, request_config, request_id, infer_streamers, @@ -559,7 +561,8 @@ def _create_chat_completion_stream_response(self, result, request_config, reques toolcall = None if output.is_finished: - toolcall = self._get_toolcall(self.template.decode(output.token_ids)) + toolcall = self._get_toolcall( + self.template.decode(output.token_ids, **infer_streamers[i].decode_kwargs)) choice = ChatCompletionResponseStreamChoice( index=i, @@ -593,7 +596,7 @@ def _create_chat_completion_response( choices = [] for output in result.outputs: output.token_ids = list(output.token_ids) - response = self.template.decode(output.token_ids) + response = self.template.decode(output.token_ids, template_inputs=inputs['template_inputs']) # Extract reasoning content if reasoning_parser is enabled reasoning_content = None @@ -743,7 +746,10 @@ def infer( if request_config.stream: def _gen_wrapper(): - infer_streamers = [InferStreamer(self.template) for _ in range(generation_config.n)] + infer_streamers = [ + InferStreamer(self.template, template_inputs=inputs['template_inputs']) + for _ in range(generation_config.n) + ] token_idxs = [0 for _ in range(generation_config.n)] while self.engine.has_unfinished_requests(): result = self.engine.step() diff --git a/swift/template/base.py b/swift/template/base.py index ead36f3e61..7dec3b1916 100644 --- a/swift/template/base.py +++ b/swift/template/base.py @@ -156,14 +156,14 @@ def __init__( if processor is not None: self.init_processor(processor) - def _get_enable_thinking(self, inputs): - enable_thinking = inputs.chat_template_kwargs.get('enable_thinking') + def _get_enable_thinking(self, inputs=None): + enable_thinking = None if inputs is None else inputs.chat_template_kwargs.get('enable_thinking') if enable_thinking is None: enable_thinking = self.enable_thinking return enable_thinking - def _get_response_prefix(self, inputs): - response_prefix = inputs.chat_template_kwargs.get('response_prefix') + def _get_response_prefix(self, inputs=None): + response_prefix = None if inputs is None else inputs.chat_template_kwargs.get('response_prefix') if response_prefix is None: response_prefix = self.response_prefix if response_prefix is not None: @@ -317,8 +317,8 @@ def _preprocess_function_call(self, inputs: StdTemplateInputs) -> None: def prepare_engine_kwargs(self) -> Dict[str, Any]: return {} - def _get_max_pixels(self, inputs): - max_pixels = inputs.chat_template_kwargs.get('max_pixels') + def _get_max_pixels(self, inputs=None): + max_pixels = None if inputs is None else inputs.chat_template_kwargs.get('max_pixels') if max_pixels is None: max_pixels = self.max_pixels return max_pixels @@ -683,13 +683,20 @@ def decode_seq_cls(self, logits: torch.Tensor, top_logprobs: int): logprobs = [self._get_seq_cls_logprobs(pred, logprobs[i], top_logprobs) for i, pred in enumerate(preds)] return preds, logprobs - def decode(self, generate_ids: List[int], *, is_finished: bool = True, first_token=True, **kwargs) -> Any: + def decode(self, + generate_ids: List[int], + *, + is_finished: bool = True, + first_token=True, + template_inputs=None, + **kwargs) -> Any: if kwargs.get('spaces_between_special_tokens') is None: kwargs['spaces_between_special_tokens'] = False generate_ids = self.skip_stop_tokens(generate_ids, is_finished) response = self.tokenizer.decode(generate_ids, **kwargs) - if first_token and self.response_prefix: - response = self.response_prefix + response + response_prefix = self._get_response_prefix(template_inputs) + if first_token and response_prefix: + response = response_prefix + response return response def decode_prm(self, input_ids: torch.Tensor, logits: torch.Tensor) -> Any: diff --git a/swift/template/template_inputs.py b/swift/template/template_inputs.py index 3fdc44b9b0..0bdeb7adab 100644 --- a/swift/template/template_inputs.py +++ b/swift/template/template_inputs.py @@ -59,12 +59,13 @@ def is_multimodal(self): def from_dict(cls, inputs: Dict[str, Any]) -> 'StdTemplateInputs': inputs = deepcopy(inputs) kwargs = {} - for key in ['label', 'channel', 'margin', 'rejected_response', 'chat_template_kwargs']: + for key in ['label', 'channel', 'margin', 'rejected_response']: if key in inputs: kwargs[key] = inputs[key] messages = inputs['messages'] tools = inputs.get('tools') objects = inputs.get('objects') or {} + chat_template_kwargs = inputs.get('chat_template_kwargs') or {} if messages and messages[0]['role'] == 'system': message = messages.pop(0) @@ -98,6 +99,7 @@ def from_dict(cls, inputs: Dict[str, Any]) -> 'StdTemplateInputs': system=system, tools=tools, objects=objects, + chat_template_kwargs=chat_template_kwargs, extra_kwargs=extra_kwargs, **kwargs, **media_kwargs) From 71e40135bb80b51202a6a0653f5df3e5c284d541 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Fri, 8 May 2026 11:08:59 +0800 Subject: [PATCH 08/13] fix --- swift/infer_engine/protocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/swift/infer_engine/protocol.py b/swift/infer_engine/protocol.py index 9f1b8cf821..9a59d6f195 100644 --- a/swift/infer_engine/protocol.py +++ b/swift/infer_engine/protocol.py @@ -234,6 +234,7 @@ class ChatCompletionRequestMixin: messages: Messages tools: Optional[List[Tool]] = None tool_choice: Optional[Union[str, Dict]] = None + chat_template_kwargs: Dict[str, Any] = field(default_factory=dict) def __post_init__(self): if self.tool_choice is None: From 7258e05d942f9adac3bb7a2cfb0b623a2340b253 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Fri, 8 May 2026 11:17:51 +0800 Subject: [PATCH 09/13] fix --- swift/template/templates/qwen.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/swift/template/templates/qwen.py b/swift/template/templates/qwen.py index 34ef62c4f4..7494c6644a 100644 --- a/swift/template/templates/qwen.py +++ b/swift/template/templates/qwen.py @@ -306,7 +306,7 @@ def init_env_args(self): self.transformers_version = version.parse(transformers.__version__) self.bbox_format = get_env_args('QWENVL_BBOX_FORMAT', str, 'legacy') - def _get_max_pixels(self, inputs): + def _get_max_pixels(self, inputs=None): return self.max_pixels def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int, @@ -675,9 +675,6 @@ def init_processor(self, processor) -> None: 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 _get_max_pixels(self, inputs): - return self.max_pixels - 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 From 441f6e41e7269cb5fd54fb64dce81372336dda47 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Fri, 8 May 2026 11:18:33 +0800 Subject: [PATCH 10/13] update --- docs/source/Customization/Custom-dataset.md | 2 +- docs/source_en/Customization/Custom-dataset.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/Customization/Custom-dataset.md b/docs/source/Customization/Custom-dataset.md index ff3a40087d..679dad4fe3 100644 --- a/docs/source/Customization/Custom-dataset.md +++ b/docs/source/Customization/Custom-dataset.md @@ -94,7 +94,7 @@ print(inputs['loss_scale']) {"messages": [..., {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"北京\"}}", "loss": false}, {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"上海\"}}"}, ...]} ``` -- "chat_template_kwargs"字段,(需ms-swift>=4.2.1),你可以通过在数据集中传入该字段**样本级别**控制template的min_pixels, max_pixels, fps等多模态参数,以及enable_thinking(推理时)等参数。以下为不同模型支持的参数: +- "chat_template_kwargs"字段,(需ms-swift>=4.3.0),你可以通过在数据集中传入该字段**样本级别**控制template的min_pixels, max_pixels, fps等多模态参数,以及enable_thinking(推理时)等参数。以下为不同模型支持的参数: - 其中"enable_thinking", "response_prefix"支持所有模型(推理时生效);"max_pixels"参数支持所有多模态模型。 - Qwen系列多模态模型:min_pixels, max_pixels, fps等qwen_vl_utils/qwen_omni_utils支持的参数。 ```jsonl diff --git a/docs/source_en/Customization/Custom-dataset.md b/docs/source_en/Customization/Custom-dataset.md index 6e5c6f7788..530e98aedb 100644 --- a/docs/source_en/Customization/Custom-dataset.md +++ b/docs/source_en/Customization/Custom-dataset.md @@ -96,7 +96,7 @@ Note: If you set "loss"/"loss_scale" on consecutive "tool_call" messages in the {"messages": [..., {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"Beijing\"}}", "loss": false}, {"role": "tool_call", "content": "{\"name\": \"realtime_aqi\", \"arguments\": {\"city\": \"Shanghai\"}}"}, ...]} ``` -- The "chat_template_kwargs" field (requires ms-swift>=4.2.1) allows you to control template multimodal parameters such as min_pixels, max_pixels, fps, as well as parameters like enable_thinking (during inference) at the **sample level** by passing this field in the dataset. The following parameters are supported by different models: +- The "chat_template_kwargs" field (requires ms-swift>=4.3.0) allows you to control template multimodal parameters such as min_pixels, max_pixels, fps, as well as parameters like enable_thinking (during inference) at the **sample level** by passing this field in the dataset. The following parameters are supported by different models: - Among them, "enable_thinking" and "response_prefix" are supported by all models (takes effect during inference); the "max_pixels" parameter is supported by all multimodal models. - Qwen series multimodal models: parameters supported by qwen_vl_utils/qwen_omni_utils such as min_pixels, max_pixels, fps, etc. From 0f0ceab2ca91a53a96c5bde96f26b270b10183ed Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Fri, 8 May 2026 11:42:19 +0800 Subject: [PATCH 11/13] fix --- .../deploy/client/llm/chat/openai_client.py | 24 ++++++++++++++++--- .../deploy/client/llm/chat/swift_client.py | 7 +++--- swift/template/templates/gemma.py | 2 +- swift/template/templates/qwen.py | 21 +++++++++++----- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/examples/deploy/client/llm/chat/openai_client.py b/examples/deploy/client/llm/chat/openai_client.py index bb3f208ba3..9a0472a9c4 100644 --- a/examples/deploy/client/llm/chat/openai_client.py +++ b/examples/deploy/client/llm/chat/openai_client.py @@ -6,7 +6,16 @@ def infer(client, model: str, messages): - resp = client.chat.completions.create(model=model, messages=messages, max_tokens=512, temperature=0) + resp = client.chat.completions.create( + model=model, + messages=messages, + max_tokens=512, + temperature=0, + extra_body={ + "chat_template_kwargs": { + "enable_thinking": False + }, + }) query = messages[0]['content'] response = resp.choices[0].message.content print(f'query: {query}') @@ -16,7 +25,16 @@ def infer(client, model: str, messages): # streaming def infer_stream(client, model: str, messages): - gen = client.chat.completions.create(model=model, messages=messages, stream=True, temperature=0) + gen = client.chat.completions.create( + model=model, + messages=messages, + stream=True, + temperature=0, + extra_body={ + "chat_template_kwargs": { + "enable_thinking": False + }, + }) print(f'messages: {messages}\nresponse: ', end='') for chunk in gen: if chunk is None: @@ -43,5 +61,5 @@ def run_client(host: str = '127.0.0.1', port: int = 8000): if __name__ == '__main__': from swift import DeployArguments, run_deploy - with run_deploy(DeployArguments(model='Qwen/Qwen2.5-1.5B-Instruct', verbose=False, log_interval=-1)) as port: + with run_deploy(DeployArguments(model='Qwen/Qwen3.5-4B', verbose=False, log_interval=-1)) as port: run_client(port=port) diff --git a/examples/deploy/client/llm/chat/swift_client.py b/examples/deploy/client/llm/chat/swift_client.py index 8e9fad399f..81deada23d 100644 --- a/examples/deploy/client/llm/chat/swift_client.py +++ b/examples/deploy/client/llm/chat/swift_client.py @@ -46,7 +46,7 @@ def run_client(host: str = '127.0.0.1', port: int = 8000): infer_batch(engine, infer_requests) messages = [{'role': 'user', 'content': 'who are you?'}] - infer_stream(engine, InferRequest(messages=messages)) + infer_stream(engine, InferRequest(messages=messages, chat_template_kwargs={'enable_thinking': False})) if __name__ == '__main__': @@ -54,7 +54,6 @@ def run_client(host: str = '127.0.0.1', port: int = 8000): run_deploy) # NOTE: In a real deployment scenario, please comment out the context of run_deploy. - with run_deploy( - DeployArguments(model='Qwen/Qwen2.5-1.5B-Instruct', verbose=False, log_interval=-1, - infer_backend='vllm')) as port: + with run_deploy(DeployArguments(model='Qwen/Qwen3.5-4B', verbose=False, log_interval=-1, + infer_backend='vllm')) as port: run_client(port=port) diff --git a/swift/template/templates/gemma.py b/swift/template/templates/gemma.py index 034d22bc91..49ac3dc243 100644 --- a/swift/template/templates/gemma.py +++ b/swift/template/templates/gemma.py @@ -265,7 +265,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int def _get_system(self, inputs: StdTemplateInputs) -> Optional[str]: system = super()._get_system(inputs) - if self.enable_thinking: + if self._get_enable_thinking(inputs): system = '<|think|>\n' + (system or '') return system diff --git a/swift/template/templates/qwen.py b/swift/template/templates/qwen.py index 7494c6644a..891ad82502 100644 --- a/swift/template/templates/qwen.py +++ b/swift/template/templates/qwen.py @@ -270,10 +270,13 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int 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=self.sampling_rate)) + audios = load_batch(inputs.audios, load_func=partial(load_audio, sampling_rate=sampling_rate)) audio_inputs = self.processor.feature_extractor( - audios, sampling_rate=self.sampling_rate, return_attention_mask=True, return_tensors='pt') + 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 @@ -679,6 +682,9 @@ 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 @@ -690,7 +696,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int return ['<|vision_start|><|image_pad|><|vision_end|>'] elif media_type == 'audio': if self.mode != 'vllm': - inputs.audios[index] = load_audio(inputs.audios[index], self.sampling_rate) + 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': @@ -706,7 +712,7 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int if video.startswith('http://') or video.startswith('https://'): import audioread video = audioread.ffdec.FFmpegAudioFile(video) - video = librosa.load(video, sr=self.sampling_rate)[0] + video = librosa.load(video, sr=sampling_rate)[0] if self.mode != 'vllm': inputs.audios.insert(inputs.audio_idx, (video, 'video')) else: @@ -992,11 +998,14 @@ def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int 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=self.sampling_rate)) + audios = load_batch(inputs.audios, load_func=partial(load_audio, sampling_rate=sampling_rate)) audio_inputs = self.processor.feature_extractor( audios, - sampling_rate=self.sampling_rate, + sampling_rate=sampling_rate, return_attention_mask=True, return_tensors='pt', padding=True, From 1bc40402e61d11077f336286e5f50f8834203c68 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Fri, 8 May 2026 11:58:54 +0800 Subject: [PATCH 12/13] update --- swift/template/templates/llm.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/swift/template/templates/llm.py b/swift/template/templates/llm.py index 0c903e3958..bc0909734f 100644 --- a/swift/template/templates/llm.py +++ b/swift/template/templates/llm.py @@ -349,16 +349,25 @@ def init_env_args(self): self.enable_thinking = self.reasoning_effort != 'no_think' self.chat_template_kwargs = {'reasoning_effort': self.reasoning_effort} + def _get_enable_thinking(self, inputs=None): + reasoning_effort = None if inputs is None else inputs.chat_template_kwargs.get('reasoning_effort') + if reasoning_effort is not None: + return reasoning_effort != 'no_think' + return super()._get_enable_thinking(inputs) + def _get_system(self, inputs): system = super()._get_system(inputs) + reasoning_effort = inputs.chat_template_kwargs.get('reasoning_effort') + if reasoning_effort is None: + reasoning_effort = self.reasoning_effort if inputs.tools: # For tool calls, append reasoning_mode after in the tool instruction system = system.replace( 'you should print ', - f'you should print <|reasoning_mode|>reasoning_effort:{self.reasoning_effort}') + f'you should print <|reasoning_mode|>reasoning_effort:{reasoning_effort}') else: # For non-tool calls, append reasoning_mode to the system/prefix area - mode_str = f'<|reasoning_mode|>reasoning_effort:{self.reasoning_effort}' + mode_str = f'<|reasoning_mode|>reasoning_effort:{reasoning_effort}' system = (system or '') + mode_str return system From e526e33364d2ade1be5aef2922086c60dc943c96 Mon Sep 17 00:00:00 2001 From: Jintao Huang Date: Fri, 8 May 2026 12:01:26 +0800 Subject: [PATCH 13/13] update --- examples/deploy/client/llm/chat/openai_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/deploy/client/llm/chat/openai_client.py b/examples/deploy/client/llm/chat/openai_client.py index 9a0472a9c4..667dd8115f 100644 --- a/examples/deploy/client/llm/chat/openai_client.py +++ b/examples/deploy/client/llm/chat/openai_client.py @@ -12,8 +12,8 @@ def infer(client, model: str, messages): max_tokens=512, temperature=0, extra_body={ - "chat_template_kwargs": { - "enable_thinking": False + 'chat_template_kwargs': { + 'enable_thinking': False }, }) query = messages[0]['content'] @@ -31,8 +31,8 @@ def infer_stream(client, model: str, messages): stream=True, temperature=0, extra_body={ - "chat_template_kwargs": { - "enable_thinking": False + 'chat_template_kwargs': { + 'enable_thinking': False }, }) print(f'messages: {messages}\nresponse: ', end='')