# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from collections.abc import Iterable, Mapping, Sequence import torch import torch.nn as nn from transformers import BatchFeature from vllm.config import VllmConfig from vllm.model_executor.models.mistral3 import ( Mistral3DummyInputsBuilder, Mistral3ForConditionalGeneration, Mistral3ImageEmbeddingInputs, Mistral3ImageInputs, Mistral3MultiModalProjector, Mistral3ProcessingInfo, init_vision_tower_for_mistral3, ) from vllm.model_executor.models.pixtral import PixtralHFEncoderInfo from vllm.model_executor.models.utils import ( AutoWeightsLoader, WeightsMapper, init_vllm_registered_model, maybe_prefix, ) from vllm.multimodal import MULTIMODAL_REGISTRY from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems from vllm.multimodal.parse import ( ImageEmbeddingItems, ImageProcessorItems, MultiModalDataItems, ) from vllm.multimodal.processing import ( BaseMultiModalProcessor, PromptReplacement, PromptUpdate, ) class LightOnOCRProcessingInfo(Mistral3ProcessingInfo): def get_vision_encoder_info( self, mm_processor_kwargs: Mapping[str, object] | None = None, ) -> PixtralHFEncoderInfo: # LightOnOCR prompt updates use vision_config.image_size directly; # preserve that behavior rather than inheriting Mistral3's processor # longest_edge override. return PixtralHFEncoderInfo(self.get_hf_config()) class LightOnOCRMultiModalProcessor(BaseMultiModalProcessor[LightOnOCRProcessingInfo]): def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str: return self.dummy_inputs.get_dummy_text(mm_counts) def _call_hf_processor( self, hf_data: Mapping[str, object], hf_kwargs: Mapping[str, object], ) -> BatchFeature: # The checkpoint sets `do_pad: null`, which disables padding since # Transformers v5.18, so images of different sizes cannot be stacked. return self.info.ctx.call_hf_processor( self.info.get_hf_processor(**hf_kwargs), hf_data, {**hf_kwargs, "do_pad": True}, ) def _postprocess_hf_mm_data( self, mm_data: Mapping[str, object], hf_processor_mm_kwargs: Mapping[str, object], processed_data: BatchFeature, ) -> BatchFeature: if not mm_data: return processed_data # un-pad pixel_values per-image so caches remain independent. pixel_values = processed_data.get("pixel_values") if pixel_values is not None: image_sizes = processed_data["image_sizes"] assert len(pixel_values) == len(image_sizes) processed_data["pixel_values"] = [ p[:, :h, :w] for p, (h, w) in zip(pixel_values, image_sizes) ] return processed_data def _get_mm_fields_config( self, hf_inputs: BatchFeature, hf_processor_mm_kwargs: Mapping[str, object], ) -> Mapping[str, MultiModalFieldConfig]: return dict( pixel_values=MultiModalFieldConfig.batched("image"), image_embeds=MultiModalFieldConfig.batched("image"), ) def _get_prompt_updates( self, mm_items: MultiModalDataItems, hf_processor_mm_kwargs: Mapping[str, object], out_mm_kwargs: MultiModalKwargsItems, ) -> Sequence[PromptUpdate]: hf_config = self.info.get_hf_config() image_token_id = hf_config.image_token_index def replace(item_idx: int): images = mm_items.get_items( "image", (ImageEmbeddingItems, ImageProcessorItems) ) if isinstance(images, ImageEmbeddingItems): num_image_tokens = images.get_feature_size(item_idx) else: assert isinstance(images, ImageProcessorItems) size = images.get_image_size(item_idx) num_image_tokens = self.info.get_num_image_tokens( image_width=size.width, image_height=size.height ) # break/end tokens are not used in LightOnOCR return [image_token_id] * num_image_tokens return [ PromptReplacement( modality="image", target=[image_token_id], replacement=replace ) ] @MULTIMODAL_REGISTRY.register_processor( LightOnOCRMultiModalProcessor, info=LightOnOCRProcessingInfo, dummy_inputs=Mistral3DummyInputsBuilder, ) class LightOnOCRForConditionalGeneration(Mistral3ForConditionalGeneration): supports_mm_device_do_normalize = False hf_to_vllm_mapper = WeightsMapper( orig_to_new_prefix={ "model.vision_encoder.": "vision_tower.", "model.vision_projection.": "multi_modal_projector.", "lm_head.": "language_model.lm_head.", "model.language_model.": "language_model.model.", } ) def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None: nn.Module.__init__(self) config = vllm_config.model_config.hf_config quant_config = vllm_config.quant_config multimodal_config = vllm_config.model_config.multimodal_config self.config = config self.multimodal_config = multimodal_config with self._mark_tower_model(vllm_config, "image"): self.vision_tower = init_vision_tower_for_mistral3( config, quant_config=quant_config, require_post_norm=False, prefix=maybe_prefix(prefix, "vision_tower"), ) self.multi_modal_projector = Mistral3MultiModalProjector( vision_hidden_size=config.vision_config.hidden_size, text_hidden_size=config.text_config.hidden_size, projector_hidden_act=config.projector_hidden_act, spatial_merge_size=config.spatial_merge_size, patch_size=config.vision_config.patch_size, multimodal_projector_bias=config.multimodal_projector_bias, quant_config=quant_config, prefix=maybe_prefix(prefix, "multi_modal_projector"), ) with self._mark_language_model(vllm_config): self.language_model = init_vllm_registered_model( vllm_config=vllm_config, hf_config=config.text_config, prefix=maybe_prefix(prefix, "language_model"), ) self.make_empty_intermediate_tensors = ( self.language_model.make_empty_intermediate_tensors ) def _parse_and_validate_image_input( self, **kwargs: object ) -> Mistral3ImageInputs | None: image_embeds = kwargs.get("image_embeds") if image_embeds is not None: return Mistral3ImageEmbeddingInputs(data=image_embeds) return super()._parse_and_validate_image_input(**kwargs) def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: loader = AutoWeightsLoader(self) return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)