main
py 205 lines 6.33 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for OpenVLA multimodal preprocessing."""
4
5 import numpy as np
6 import pytest
7 import torch
8 from PIL import Image
9 from transformers import LlamaConfig
10
11 from vllm.model_executor.models.openvla import (
12 OpenVLAForActionPrediction,
13 OpenVLAMultiModalProcessor,
14 OpenVLAProcessingInfo,
15 )
16 from vllm.multimodal.parse import ImageProcessorItems, MultiModalDataItems
17 from vllm.transformers_utils.configs.openvla import OpenVLAConfig
18 from vllm.transformers_utils.processors.openvla import (
19 IMAGENET_MEAN,
20 IMAGENET_STD,
21 SIGLIP_MEAN,
22 SIGLIP_STD,
23 OpenVLAImageProcessor,
24 OpenVLAProcessor,
25 preprocess_openvla_image,
26 to_rgb_image,
27 )
28
29 pytestmark = pytest.mark.cpu_test
30
31
32 class _FakeTokenizer:
33 bos_token_id = 1
34 init_kwargs: dict[str, object] = {}
35
36 def encode(self, prompt: str, **kwargs: object) -> list[int]:
37 assert prompt == "In: test\nOut:"
38 if kwargs == {"add_special_tokens": True}:
39 return [self.bos_token_id, 10, 11]
40 assert kwargs == {"add_special_tokens": False}
41 return [10, 11]
42
43 def __call__(self, text: str, **kwargs: object) -> dict[str, list[list[int]]]:
44 return {"input_ids": [self.encode(text, **kwargs)]}
45
46
47 class _FakeProcessingInfo:
48 def __init__(self) -> None:
49 self.config = OpenVLAConfig()
50
51 def get_hf_config(self) -> OpenVLAConfig:
52 return self.config
53
54 def get_tokenizer(self) -> _FakeTokenizer:
55 return _FakeTokenizer()
56
57 def get_num_image_tokens(self, *, image_width: int, image_height: int) -> int:
58 assert image_width > 0
59 assert image_height > 0
60 return 256
61
62
63 class _FakeOpenVLAProcessingInfo(OpenVLAProcessingInfo):
64 def get_hf_config(self) -> OpenVLAConfig:
65 return OpenVLAConfig()
66
67
68 def _make_processor() -> OpenVLAMultiModalProcessor:
69 processor = OpenVLAMultiModalProcessor.__new__(OpenVLAMultiModalProcessor)
70 processor.info = _FakeProcessingInfo()
71 return processor
72
73
74 def test_openvla_config_converts_text_config_dict() -> None:
75 config = OpenVLAConfig(
76 text_config={
77 "vocab_size": 123,
78 "hidden_size": 64,
79 "intermediate_size": 128,
80 "num_hidden_layers": 2,
81 "num_attention_heads": 4,
82 },
83 )
84
85 assert isinstance(config.text_config, LlamaConfig)
86 assert config.text_config.vocab_size == 123
87 assert config.text_config.hidden_size == 64
88 assert config.text_config.architectures == ["LlamaForCausalLM"]
89
90
91 @pytest.mark.parametrize(
92 ("image", "expected_size", "expected_pixel"),
93 [
94 (
95 torch.tensor(
96 [
97 [[1.0, 1.0], [1.0, 1.0]],
98 [[0.0, 0.0], [0.0, 0.0]],
99 [[0.0, 0.0], [0.0, 0.0]],
100 ]
101 ),
102 (2, 2),
103 (255, 0, 0),
104 ),
105 (
106 np.full((4, 5, 1), 128, dtype=np.uint8),
107 (5, 4),
108 (128, 128, 128),
109 ),
110 ],
111 )
112 def test_openvla_to_rgb_image(
113 image: torch.Tensor | np.ndarray,
114 expected_size: tuple[int, int],
115 expected_pixel: tuple[int, int, int],
116 ) -> None:
117 rgb_image = to_rgb_image(image)
118
119 assert rgb_image.mode == "RGB"
120 assert rgb_image.size == expected_size
121 assert rgb_image.getpixel((0, 0)) == expected_pixel
122
123
124 def test_openvla_preprocess_image_matches_expected_normalization() -> None:
125 image = Image.fromarray(
126 np.arange(12 * 10 * 3, dtype=np.uint8).reshape(10, 12, 3),
127 mode="RGB",
128 )
129
130 pixel_values = preprocess_openvla_image(image, image_size=224)
131
132 resized = image.resize((224, 224), Image.Resampling.BICUBIC)
133 raw = np.asarray(resized, dtype=np.float32) / 255.0
134 expected_dinov2 = ((raw - IMAGENET_MEAN) / IMAGENET_STD).transpose(2, 0, 1)
135 expected_siglip = ((raw - SIGLIP_MEAN) / SIGLIP_STD).transpose(2, 0, 1)
136 expected = np.concatenate([expected_dinov2, expected_siglip], axis=0)
137
138 assert pixel_values.shape == (6, 224, 224)
139 assert pixel_values.dtype == torch.float32
140 torch.testing.assert_close(pixel_values, torch.from_numpy(expected))
141
142
143 def test_openvla_processor_outputs_pixel_values() -> None:
144 processor = OpenVLAProcessor(
145 image_processor=OpenVLAImageProcessor(image_size=224),
146 tokenizer=_FakeTokenizer(),
147 )
148 image = Image.new("RGB", (8, 8), color=(255, 0, 0))
149
150 batch = processor(
151 text="In: test\nOut:",
152 images=image,
153 text_kwargs={"add_special_tokens": True},
154 )
155
156 assert batch["input_ids"] == [[1, 10, 11]]
157 assert batch["pixel_values"].shape == (1, 6, 224, 224)
158 assert batch["pixel_values"].dtype == torch.float32
159
160
161 def test_openvla_image_processor_outputs_pixel_values() -> None:
162 processor = OpenVLAImageProcessor(image_size=224)
163 image = Image.new("RGB", (8, 8), color=(255, 0, 0))
164
165 output = processor(images=image)
166
167 assert output["pixel_values"].shape == (1, 6, 224, 224)
168 assert output["pixel_values"].dtype == torch.float32
169
170
171 def test_openvla_processing_info_token_counts() -> None:
172 info = _FakeOpenVLAProcessingInfo.__new__(_FakeOpenVLAProcessingInfo)
173
174 assert info.get_supported_mm_limits() == {"image": 1}
175 assert info.get_num_image_tokens(image_width=640, image_height=480) == 256
176 assert info.get_image_size_with_most_features().width == 224
177 assert info.get_image_size_with_most_features().height == 224
178 assert info.get_mm_max_tokens_per_item(seq_len=2048, mm_counts={"image": 1}) == {
179 "image": 256
180 }
181
182
183 def test_openvla_prompt_update_inserts_image_tokens_after_bos() -> None:
184 processor = _make_processor()
185 image = Image.new("RGB", (640, 480), color=(255, 255, 255))
186 mm_items = MultiModalDataItems({"image": ImageProcessorItems([image])})
187
188 prompt_update = processor._get_prompt_updates(mm_items, {}, {})[0]
189 resolved = prompt_update.resolve(0)
190 content = resolved.content
191
192 assert resolved.modality == "image"
193 assert [
194 (match.start_idx, match.end_idx)
195 for match in resolved.iter_token_matches([1, 10, 11])
196 ] == [(1, 1)]
197 assert content.full == [32000] * 256
198
199 is_embed = content.is_embed(content.full)
200 assert is_embed.dtype == torch.bool
201 assert is_embed.tolist() == [True] * 256
202
203
204 def test_openvla_placeholder_string() -> None:
205 assert OpenVLAForActionPrediction.get_placeholder_str("image", 0) is None