fix: fix mm processor double bos (#26505)

This commit is contained in:
Yihao Wang
2026-07-11 07:53:08 +08:00
committed by GitHub
parent 649ce5dd3d
commit 7de33ce806
2 changed files with 57 additions and 0 deletions
@@ -204,6 +204,12 @@ class BaseMultimodalProcessor(ABC):
else: else:
self._tokenizer = self._processor self._tokenizer = self._processor
# Same guard as in serving_chat.py against double BOS.
try:
self._tokenizer_auto_adds_specials = len(self._tokenizer.encode("")) > 0
except Exception:
self._tokenizer_auto_adds_specials = False
# FIXME: not accurate, model and image specific # FIXME: not accurate, model and image specific
self.NUM_TOKEN_PER_FRAME = 330 self.NUM_TOKEN_PER_FRAME = 330
@@ -467,6 +473,12 @@ class BaseMultimodalProcessor(ABC):
npu_apply_glm46v_image_preprocess_patch() npu_apply_glm46v_image_preprocess_patch()
kwargs["device"] = "npu" kwargs["device"] = "npu"
# Avoid double BOS when the chat template already wrote one.
if self._tokenizer_auto_adds_specials and isinstance(input_text, str):
bos = getattr(self._tokenizer, "bos_token", None)
if bos and input_text.startswith(bos):
kwargs.setdefault("add_special_tokens", False)
result = processor.__call__( result = processor.__call__(
text=[input_text], text=[input_text],
padding=True, padding=True,
@@ -138,6 +138,8 @@ class TestProcessMmDataKwargs(unittest.TestCase):
proc.disable_fast_image_processor = server_args.disable_fast_image_processor proc.disable_fast_image_processor = server_args.disable_fast_image_processor
proc.skip_tokenizer_init = server_args.skip_tokenizer_init proc.skip_tokenizer_init = server_args.skip_tokenizer_init
proc._processor = mock_processor proc._processor = mock_processor
proc._tokenizer = MagicMock()
proc._tokenizer_auto_adds_specials = False
proc.image_config = mm_process_config.get("image", {}) proc.image_config = mm_process_config.get("image", {})
proc.video_config = mm_process_config.get("video", {}) proc.video_config = mm_process_config.get("video", {})
proc.audio_config = mm_process_config.get("audio", {}) proc.audio_config = mm_process_config.get("audio", {})
@@ -296,5 +298,48 @@ class TestOverrideProcessorsConfigInjection(unittest.TestCase):
self.assertTrue(audio_kw.get("truncation")) self.assertTrue(audio_kw.get("truncation"))
class TestDoubleBosGuard(unittest.TestCase):
"""Regression test for the multimodal double-BOS bug.
Repro condition (Cohere2 / Llama3-LLaVA-Next family):
- tokenizer.encode("") returns [bos_id] (auto-adds specials), AND
- chat template renders the BOS string as a literal at the start.
Without the guard in BaseMultimodalProcessor, the inner processor.__call__
on the rendered prompt would auto-prepend a second BOS, producing 2 leading
BOS tokens vs the HF reference's 1.
"""
def test_guard_passes_add_special_tokens_false_on_bug_condition(self):
from sglang.srt.multimodal.processors.base_processor import (
BaseMultimodalProcessor,
)
server_args = MagicMock()
server_args.mm_process_config = {}
server_args.disable_fast_image_processor = True
server_args.keep_mm_feature_on_device = True
mock_hf_processor = MagicMock()
mock_hf_processor.__class__.__name__ = "TestProcessor"
mock_hf_processor.__call__ = MagicMock(return_value={})
mock_hf_processor.tokenizer.encode = MagicMock(return_value=[2])
mock_hf_processor.tokenizer.bos_token = "<BOS>"
with patch.object(BaseMultimodalProcessor, "__abstractmethods__", set()):
proc = BaseMultimodalProcessor(
hf_config=MagicMock(),
server_args=server_args,
_processor=mock_hf_processor,
transport_mode=None,
)
proc.FEATURE_NAMES = []
proc.process_mm_data("<BOS>hello", images=["img1"])
call_kwargs = mock_hf_processor.__call__.call_args.kwargs
self.assertEqual(call_kwargs.get("add_special_tokens"), False)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()