| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from typing import Any |
| 5 | |
| 6 | import pytest |
| 7 | import torch |
| 8 | from transformers import AutoModelForImageTextToText |
| 9 | |
| 10 | from vllm.platforms import current_platform |
| 11 | from vllm.utils.gpu_sync_debug import gpu_sync_allowed |
| 12 | |
| 13 | from ....conftest import HfRunner, ImageTestAssets, VllmRunner |
| 14 | from .vlm_utils import model_utils |
| 15 | |
| 16 | MODEL = "google/gemma-3-4b-it" |
| 17 | PROMPT = ( |
| 18 | "<bos><start_of_turn>user\n" |
| 19 | "<start_of_image>What is the content in the center of the image?" |
| 20 | "<end_of_turn>\n<start_of_turn>model\n" |
| 21 | ) |
| 22 | |
| 23 | |
| 24 | def _install_prefill_hidden_capture(model): |
| 25 | model = getattr(model, "module", model) |
| 26 | model._prefill_hidden = None |
| 27 | |
| 28 | language_model = model.language_model.model |
| 29 | original_forward = language_model.forward |
| 30 | |
| 31 | def forward(*args, **kwargs): |
| 32 | hidden_states = original_forward(*args, **kwargs) |
| 33 | if model._prefill_hidden is None and torch.is_tensor(hidden_states): |
| 34 | # Capturing hidden states for comparison is a deliberate D2H. |
| 35 | with gpu_sync_allowed(): |
| 36 | model._prefill_hidden = hidden_states.detach().float().cpu() |
| 37 | return hidden_states |
| 38 | |
| 39 | language_model.forward = forward |
| 40 | |
| 41 | |
| 42 | def _get_prefill_hidden(model): |
| 43 | model = getattr(model, "module", model) |
| 44 | hidden = getattr(model, "_prefill_hidden", None) |
| 45 | assert hidden is not None |
| 46 | return hidden |
| 47 | |
| 48 | |
| 49 | def _get_hf_prefill_hidden(hf_model: HfRunner, image: Any): |
| 50 | inputs = hf_model.get_inputs([PROMPT], images=[image])[0] |
| 51 | with torch.no_grad(): |
| 52 | outputs = hf_model.model.model( |
| 53 | **hf_model.wrap_device(inputs), |
| 54 | use_cache=False, |
| 55 | ) |
| 56 | return outputs.last_hidden_state[0].detach().float().cpu() |
| 57 | |
| 58 | |
| 59 | def _get_vllm_prefill_hidden( |
| 60 | vllm_runner: type[VllmRunner], |
| 61 | image: Any, |
| 62 | vllm_runner_kwargs: dict[str, Any], |
| 63 | ): |
| 64 | with vllm_runner( |
| 65 | MODEL, |
| 66 | max_model_len=4096, |
| 67 | max_num_seqs=2, |
| 68 | enforce_eager=True, |
| 69 | limit_mm_per_prompt={"image": 1}, |
| 70 | **vllm_runner_kwargs, |
| 71 | ) as vllm_model: |
| 72 | vllm_model.apply_model(_install_prefill_hidden_capture) |
| 73 | vllm_model.generate_greedy([PROMPT], max_tokens=1, images=[image]) |
| 74 | return vllm_model.apply_model(_get_prefill_hidden)[0] |
| 75 | |
| 76 | |
| 77 | @pytest.mark.core_model |
| 78 | @pytest.mark.skipif( |
| 79 | current_platform.is_rocm(), reason="ROCm attention has accuracy issue for this test" |
| 80 | ) |
| 81 | def test_mm_prefix_lm_e2e( |
| 82 | hf_runner: type[HfRunner], |
| 83 | vllm_runner: type[VllmRunner], |
| 84 | image_assets: ImageTestAssets, |
| 85 | monkeypatch: pytest.MonkeyPatch, |
| 86 | ): |
| 87 | """Regression: Gemma3 native prefill must apply image prefix-LM mask.""" |
| 88 | monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1") |
| 89 | image = image_assets[0].pil_image |
| 90 | |
| 91 | vllm_runner_kwargs: dict[str, Any] = { |
| 92 | "mm_processor_cache_gb": 0, |
| 93 | "mm_processor_kwargs": {"do_pan_and_scan": True}, |
| 94 | } |
| 95 | vllm_hidden = _get_vllm_prefill_hidden(vllm_runner, image, vllm_runner_kwargs) |
| 96 | |
| 97 | hf_model = hf_runner( |
| 98 | MODEL, |
| 99 | auto_cls=AutoModelForImageTextToText, |
| 100 | ) |
| 101 | hf_model = model_utils.gemma3_patch_hf_runner(hf_model) |
| 102 | |
| 103 | with hf_model: |
| 104 | hf_hidden = _get_hf_prefill_hidden(hf_model, image) |
| 105 | |
| 106 | assert vllm_hidden.shape == hf_hidden.shape |
| 107 | |
| 108 | full_cos = torch.nn.functional.cosine_similarity( |
| 109 | vllm_hidden.flatten(), hf_hidden.flatten(), dim=0 |
| 110 | ) |
| 111 | image_cos = torch.nn.functional.cosine_similarity( |
| 112 | vllm_hidden[1:769].flatten(), hf_hidden[1:769].flatten(), dim=0 |
| 113 | ) |
| 114 | |
| 115 | assert full_cos > 0.9, ( |
| 116 | "Gemma3 mm-prefix-LM full prefill hidden states should be close to HF; " |
| 117 | f"got {full_cos=}" |
| 118 | ) |
| 119 | assert image_cos > 0.9, ( |
| 120 | "Gemma3 mm-prefix-LM image prefill hidden states should be close to HF; " |
| 121 | f"got {image_cos=}" |
| 122 | ) |