From 7258c00575010782b809762040e548aaef4f91a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Gallou=C3=A9dec?= Date: Wed, 15 Apr 2026 16:08:01 +0000 Subject: [PATCH 1/5] Check prefix preservation at the token level --- trl/chat_template_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/trl/chat_template_utils.py b/trl/chat_template_utils.py index c7cbbb1e02e..d863314169e 100644 --- a/trl/chat_template_utils.py +++ b/trl/chat_template_utils.py @@ -420,18 +420,18 @@ def is_chat_template_prefix_preserving(tokenizer: PreTrainedTokenizer) -> bool: ] try: - text1 = tokenizer.apply_chat_template(messages1, tokenize=False) - text2 = tokenizer.apply_chat_template(messages2, tokenize=False, add_generation_prompt=True) + ids1 = tokenizer.apply_chat_template(messages1, tokenize=True, return_dict=False) + ids2 = tokenizer.apply_chat_template(messages2, tokenize=True, return_dict=False, add_generation_prompt=True) except TypeError: # Best-effort fallback for templates that reject dict args (e.g. DeepSeek-V3). This is a chat template # bug (see transformers#45419), and the training chat template fixes it to avoid blocking users. dummy_tool_calls = [{"type": "function", "function": {"name": "dummy", "arguments": "{}"}}] messages1[1]["tool_calls"] = dummy_tool_calls messages2[1]["tool_calls"] = dummy_tool_calls - text1 = tokenizer.apply_chat_template(messages1, tokenize=False) - text2 = tokenizer.apply_chat_template(messages2, tokenize=False, add_generation_prompt=True) + ids1 = tokenizer.apply_chat_template(messages1, tokenize=True, return_dict=False) + ids2 = tokenizer.apply_chat_template(messages2, tokenize=True, return_dict=False, add_generation_prompt=True) - return text2.startswith(text1) + return ids2[: len(ids1)] == ids1 deepseekv3_training_chat_template = (_CHAT_TEMPLATES_DIR / "deepseekv3_training.jinja").read_text() From a076c4a46a4cb061b78c079dbc7f7546fe2360af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Gallou=C3=A9dec?= Date: Wed, 15 Apr 2026 17:39:21 +0000 Subject: [PATCH 2/5] fix vml --- trl/chat_template_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/trl/chat_template_utils.py b/trl/chat_template_utils.py index d863314169e..291697a9ebe 100644 --- a/trl/chat_template_utils.py +++ b/trl/chat_template_utils.py @@ -419,6 +419,13 @@ def is_chat_template_prefix_preserving(tokenizer: PreTrainedTokenizer) -> bool: {"role": "tool", "name": "dummy", "content": "dummy"}, ] + if isinstance(tokenizer, ProcessorMixin): + from PIL import Image + + dummy_image = Image.new("RGB", (8, 8)) + messages1 = prepare_multimodal_messages(messages1, images=[dummy_image]) + messages2 = prepare_multimodal_messages(messages2, images=[dummy_image]) + try: ids1 = tokenizer.apply_chat_template(messages1, tokenize=True, return_dict=False) ids2 = tokenizer.apply_chat_template(messages2, tokenize=True, return_dict=False, add_generation_prompt=True) From 84e90a01561ed7405cad51b8b5112c97c84005ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Gallou=C3=A9dec?= Date: Thu, 16 Apr 2026 16:39:50 +0000 Subject: [PATCH 3/5] fix vlm --- trl/chat_template_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/trl/chat_template_utils.py b/trl/chat_template_utils.py index 291697a9ebe..635733e6073 100644 --- a/trl/chat_template_utils.py +++ b/trl/chat_template_utils.py @@ -419,7 +419,8 @@ def is_chat_template_prefix_preserving(tokenizer: PreTrainedTokenizer) -> bool: {"role": "tool", "name": "dummy", "content": "dummy"}, ] - if isinstance(tokenizer, ProcessorMixin): + is_vlm = isinstance(tokenizer, ProcessorMixin) + if is_vlm: from PIL import Image dummy_image = Image.new("RGB", (8, 8)) @@ -438,6 +439,11 @@ def is_chat_template_prefix_preserving(tokenizer: PreTrainedTokenizer) -> bool: ids1 = tokenizer.apply_chat_template(messages1, tokenize=True, return_dict=False) ids2 = tokenizer.apply_chat_template(messages2, tokenize=True, return_dict=False, add_generation_prompt=True) + # VLM processors return batched output (list of lists), unbatch for single conversation + if is_vlm: + ids1 = ids1[0] + ids2 = ids2[0] + return ids2[: len(ids1)] == ids1 From 5f3f91b48fba956bc30c7c0846f1ee5f1540455f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Gallou=C3=A9dec?= Date: Fri, 17 Apr 2026 18:35:34 +0000 Subject: [PATCH 4/5] fix --- trl/chat_template_utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/trl/chat_template_utils.py b/trl/chat_template_utils.py index 919224ef124..6cc508b8227 100644 --- a/trl/chat_template_utils.py +++ b/trl/chat_template_utils.py @@ -486,7 +486,7 @@ def is_chat_template_prefix_preserving(processing_class: PreTrainedTokenizer | P messages1 = prepare_multimodal_messages(messages1, images=[dummy_image]) messages2 = prepare_multimodal_messages(messages2, images=[dummy_image]) - is_vlm = isinstance(tokenizer, ProcessorMixin) + is_vlm = isinstance(processing_class, ProcessorMixin) if is_vlm: from PIL import Image @@ -495,16 +495,20 @@ def is_chat_template_prefix_preserving(processing_class: PreTrainedTokenizer | P messages2 = prepare_multimodal_messages(messages2, images=[dummy_image]) try: - ids1 = tokenizer.apply_chat_template(messages1, tokenize=True, return_dict=False) - ids2 = tokenizer.apply_chat_template(messages2, tokenize=True, return_dict=False, add_generation_prompt=True) + ids1 = processing_class.apply_chat_template(messages1, tokenize=True, return_dict=False) + ids2 = processing_class.apply_chat_template( + messages2, tokenize=True, return_dict=False, add_generation_prompt=True + ) except TypeError: # Best-effort fallback for templates that reject dict args (e.g. DeepSeek-V3). This is a chat template # bug (see transformers#45419), and the training chat template fixes it to avoid blocking users. dummy_tool_calls = [{"type": "function", "function": {"name": "dummy", "arguments": "{}"}}] messages1[1]["tool_calls"] = dummy_tool_calls messages2[1]["tool_calls"] = dummy_tool_calls - ids1 = tokenizer.apply_chat_template(messages1, tokenize=True, return_dict=False) - ids2 = tokenizer.apply_chat_template(messages2, tokenize=True, return_dict=False, add_generation_prompt=True) + ids1 = processing_class.apply_chat_template(messages1, tokenize=True, return_dict=False) + ids2 = processing_class.apply_chat_template( + messages2, tokenize=True, return_dict=False, add_generation_prompt=True + ) # VLM processors return batched output (list of lists), unbatch for single conversation if is_vlm: From d3f247438f00790588e789c62ecfd1e003bb92c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Gallou=C3=A9dec?= Date: Fri, 17 Apr 2026 18:36:33 +0000 Subject: [PATCH 5/5] rm duplicate --- trl/chat_template_utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/trl/chat_template_utils.py b/trl/chat_template_utils.py index 6cc508b8227..76ddcf3bf9a 100644 --- a/trl/chat_template_utils.py +++ b/trl/chat_template_utils.py @@ -479,13 +479,6 @@ def is_chat_template_prefix_preserving(processing_class: PreTrainedTokenizer | P ] # VLM processors expect structured list-of-blocks content, and image-token expansion only kicks in when an image # is actually present, so include a dummy image to exercise the real code path. - if isinstance(processing_class, ProcessorMixin): - from PIL import Image - - dummy_image = Image.new("RGB", (8, 8)) - messages1 = prepare_multimodal_messages(messages1, images=[dummy_image]) - messages2 = prepare_multimodal_messages(messages2, images=[dummy_image]) - is_vlm = isinstance(processing_class, ProcessorMixin) if is_vlm: from PIL import Image