Files
vLLM/tests/models/multimodal/processing/test_mistral3.py
T
Oliver Holworthyandmergify[bot] <37929162+mergify[bot]@users.noreply.github.com> 9598487bc1 [Bugfix][Model] Mistral3: align placeholder grid with public processor (#53758)
Signed-off-by: Oliver Holworthy <1216955+oliverholworthy@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-09-21 06:36:59 +00:00

166 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
)