# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Tests for Mistral3's multimodal preprocessing kwargs.""" import pytest import torch from PIL import Image from transformers import AutoProcessor, BatchFeature from vllm.model_executor.models.lightonocr import LightOnOCRProcessingInfo from vllm.model_executor.models.mistral3 import Mistral3HFEncoderInfo from vllm.model_executor.models.pixtral import PixtralHFEncoderInfo from vllm.multimodal import MULTIMODAL_REGISTRY from vllm.multimodal.inputs import MultiModalKwargsItems from ...utils import build_model_context # This repo ships both params.json (Mistral) and config.json (HF). Auto config # selects PixtralForConditionalGeneration; force HF to exercise Mistral3. _MODEL_CONFIG_KWARGS = {"config_format": "hf"} _MODEL_ID = "mistralai/Mistral-Small-3.1-24B-Instruct-2503" _LIGHTON_MODEL_ID = "lightonai/LightOnOCR-1B-1025" def _process_images_with_hf( hf_processor, images: list[Image.Image], mm_processor_kwargs: dict[str, object], ) -> tuple[list[torch.Tensor], list[int]]: """Process images and placeholders through the public HF processor.""" hf_out = hf_processor( text=hf_processor.image_token * len(images), images=images, return_tensors="pt", **mm_processor_kwargs, ) pixel_values = hf_out["pixel_values"] image_sizes = hf_out["image_sizes"] unpadded = [p[:, :h, :w] for p, (h, w) in zip(pixel_values, image_sizes)] special_token_ids = { hf_processor.image_token_id, hf_processor.image_break_token_id, hf_processor.image_end_token_id, } placeholder_tokens = [ token_id for token_id in hf_out["input_ids"][0].tolist() if token_id in special_token_ids ] return unpadded, placeholder_tokens def _placeholder_tokens_from_prompt_updates( processor, images: list[Image.Image], pixel_values: list[torch.Tensor], mm_processor_kwargs: dict[str, object], ) -> list[int]: hf_inputs = BatchFeature({"pixel_values": pixel_values}) fields_config = processor._get_mm_fields_config(hf_inputs, mm_processor_kwargs) out_mm_kwargs = MultiModalKwargsItems.from_hf_inputs(hf_inputs, fields_config) # Prompt updates use raw PIL sizes and must predict the processed grid. mm_items = processor.info.parse_mm_data({"image": images}) updates = processor._get_prompt_updates( mm_items, mm_processor_kwargs, out_mm_kwargs ) placeholder_tokens: list[int] = [] for item_idx in range(len(images)): details = updates[0].resolve(item_idx).content placeholder_tokens.extend(details.full) return placeholder_tokens def _expected_placeholder_tokens_per_image( hf_processor, pixel_values: torch.Tensor, ) -> int: """Count projected tokens from the actual HF-processed H×W.""" image_h, image_w = pixel_values.shape[-2:] patch_size = hf_processor.image_processor.patch_size if isinstance(patch_size, dict): patch_h = patch_size["height"] patch_w = patch_size["width"] else: patch_h = patch_w = int(patch_size) spatial_merge_size = getattr(hf_processor, "spatial_merge_size", 1) merged_patch_h = patch_h * spatial_merge_size merged_patch_w = patch_w * spatial_merge_size assert image_h % merged_patch_h == 0 assert image_w % merged_patch_w == 0 return (image_h // merged_patch_h) * (image_w // merged_patch_w) @pytest.mark.parametrize("model_id", [_MODEL_ID]) @pytest.mark.parametrize( ("mm_processor_kwargs", "image_size", "expected_toks_per_img"), [ ({}, (448, 448), 256), ({"size": {"longest_edge": 1008}}, (1540, 1540), 1296), ({"size": {"longest_edge": 1288}}, (1536, 1187), 1656), ({"size": {"longest_edge": 1008}}, (29, 29), 4), ({"size": {"longest_edge": 1000}}, (1540, 1700), 1188), ], ) @pytest.mark.parametrize("num_imgs", [1, 2]) @pytest.mark.parametrize("kwargs_on_init", [True, False]) def test_processor_size_override( model_id: str, mm_processor_kwargs: dict[str, object], image_size: tuple[int, int], expected_toks_per_img: int, num_imgs: int, kwargs_on_init: bool, ): ctx = build_model_context( model_id, mm_processor_kwargs=mm_processor_kwargs if kwargs_on_init else None, limit_mm_per_prompt={"image": num_imgs}, model_config_kwargs=_MODEL_CONFIG_KWARGS, ) processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config) hf_processor_mm_kwargs = {} if kwargs_on_init else mm_processor_kwargs hf_processor = AutoProcessor.from_pretrained( model_id, fix_mistral_regex=True, ) dummy_image = Image.new("RGB", image_size, color=(127, 127, 127)) images = [dummy_image] * num_imgs merged_mm_kwargs = processor.info.ctx.get_merged_mm_kwargs(hf_processor_mm_kwargs) pixel_values, hf_placeholder_tokens = _process_images_with_hf( hf_processor, images, merged_mm_kwargs ) prompt_update_tokens = _placeholder_tokens_from_prompt_updates( processor, images, pixel_values, hf_processor_mm_kwargs ) expected_from_pixel_values = _expected_placeholder_tokens_per_image( hf_processor, pixel_values[0] ) assert expected_from_pixel_values == expected_toks_per_img assert hf_placeholder_tokens.count(hf_processor.image_token_id) == ( expected_from_pixel_values * num_imgs ) assert prompt_update_tokens == hf_placeholder_tokens def test_lightonocr_keeps_vision_config_image_size(): ctx = build_model_context( _LIGHTON_MODEL_ID, mm_processor_kwargs={"size": {"longest_edge": 1008}}, model_config_kwargs=_MODEL_CONFIG_KWARGS, ) processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config) assert isinstance(processor.info, LightOnOCRProcessingInfo) encoder_info = processor.info.get_vision_encoder_info() assert isinstance(encoder_info, PixtralHFEncoderInfo) assert not isinstance(encoder_info, Mistral3HFEncoderInfo) assert encoder_info.get_image_size() == ( processor.info.get_hf_config().vision_config.image_size )