211 lines
6.5 KiB
Python
211 lines
6.5 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Tests for OpenVLA multimodal preprocessing."""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
from PIL import Image
|
|
from transformers import LlamaConfig
|
|
|
|
from vllm.model_executor.models.openvla import (
|
|
OpenVLAForActionPrediction,
|
|
OpenVLAMultiModalProcessor,
|
|
OpenVLAProcessingInfo,
|
|
)
|
|
from vllm.multimodal.parse import ImageProcessorItems, MultiModalDataItems
|
|
from vllm.transformers_utils.configs.openvla import OpenVLAConfig
|
|
from vllm.transformers_utils.processors.openvla import (
|
|
IMAGENET_MEAN,
|
|
IMAGENET_STD,
|
|
SIGLIP_MEAN,
|
|
SIGLIP_STD,
|
|
OpenVLAImageProcessor,
|
|
OpenVLAProcessor,
|
|
preprocess_openvla_image,
|
|
to_rgb_image,
|
|
)
|
|
|
|
pytestmark = pytest.mark.cpu_test
|
|
|
|
|
|
class _FakeTokenizer:
|
|
bos_token_id = 1
|
|
init_kwargs: dict[str, object] = {}
|
|
|
|
def encode(self, prompt: str, **kwargs: object) -> list[int]:
|
|
assert prompt == "In: test\nOut:"
|
|
if kwargs == {"add_special_tokens": True}:
|
|
return [self.bos_token_id, 10, 11]
|
|
assert kwargs == {"add_special_tokens": False}
|
|
return [10, 11]
|
|
|
|
def __call__(self, text: str, **kwargs: object) -> dict[str, list[list[int]]]:
|
|
return {"input_ids": [self.encode(text, **kwargs)]}
|
|
|
|
|
|
class _FakeProcessingInfo:
|
|
def __init__(self) -> None:
|
|
self.config = OpenVLAConfig()
|
|
|
|
def get_hf_config(self) -> OpenVLAConfig:
|
|
return self.config
|
|
|
|
def get_tokenizer(self) -> _FakeTokenizer:
|
|
return _FakeTokenizer()
|
|
|
|
def get_num_image_tokens(self, *, image_width: int, image_height: int) -> int:
|
|
assert image_width > 0
|
|
assert image_height > 0
|
|
return 256
|
|
|
|
|
|
class _FakeOpenVLAProcessingInfo(OpenVLAProcessingInfo):
|
|
def get_hf_config(self) -> OpenVLAConfig:
|
|
return OpenVLAConfig()
|
|
|
|
|
|
def _make_processor() -> OpenVLAMultiModalProcessor:
|
|
processor = OpenVLAMultiModalProcessor.__new__(OpenVLAMultiModalProcessor)
|
|
processor.info = _FakeProcessingInfo()
|
|
return processor
|
|
|
|
|
|
def test_openvla_config_converts_text_config_dict() -> None:
|
|
config = OpenVLAConfig(
|
|
text_config={
|
|
"vocab_size": 123,
|
|
"hidden_size": 64,
|
|
"intermediate_size": 128,
|
|
"num_hidden_layers": 2,
|
|
"num_attention_heads": 4,
|
|
},
|
|
)
|
|
|
|
assert isinstance(config.text_config, LlamaConfig)
|
|
assert config.text_config.vocab_size == 123
|
|
assert config.text_config.hidden_size == 64
|
|
assert config.text_config.architectures == ["LlamaForCausalLM"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("image", "expected_size", "expected_pixel"),
|
|
[
|
|
(
|
|
torch.tensor(
|
|
[
|
|
[[1.0, 1.0], [1.0, 1.0]],
|
|
[[0.0, 0.0], [0.0, 0.0]],
|
|
[[0.0, 0.0], [0.0, 0.0]],
|
|
]
|
|
),
|
|
(2, 2),
|
|
(255, 0, 0),
|
|
),
|
|
(
|
|
np.full((4, 5, 1), 128, dtype=np.uint8),
|
|
(5, 4),
|
|
(128, 128, 128),
|
|
),
|
|
],
|
|
)
|
|
def test_openvla_to_rgb_image(
|
|
image: torch.Tensor | np.ndarray,
|
|
expected_size: tuple[int, int],
|
|
expected_pixel: tuple[int, int, int],
|
|
) -> None:
|
|
rgb_image = to_rgb_image(image)
|
|
|
|
assert rgb_image.mode == "RGB"
|
|
assert rgb_image.size == expected_size
|
|
assert rgb_image.getpixel((0, 0)) == expected_pixel
|
|
|
|
|
|
def test_openvla_preprocess_image_matches_expected_normalization() -> None:
|
|
image = Image.fromarray(
|
|
np.arange(12 * 10 * 3, dtype=np.uint8).reshape(10, 12, 3),
|
|
mode="RGB",
|
|
)
|
|
|
|
pixel_values = preprocess_openvla_image(image, image_size=224)
|
|
|
|
resized = image.resize((224, 224), Image.Resampling.BICUBIC)
|
|
raw = np.asarray(resized, dtype=np.float32) / 255.0
|
|
expected_dinov2 = ((raw - IMAGENET_MEAN) / IMAGENET_STD).transpose(2, 0, 1)
|
|
expected_siglip = ((raw - SIGLIP_MEAN) / SIGLIP_STD).transpose(2, 0, 1)
|
|
expected = np.concatenate([expected_dinov2, expected_siglip], axis=0)
|
|
|
|
assert pixel_values.shape == (6, 224, 224)
|
|
assert pixel_values.dtype == torch.float32
|
|
torch.testing.assert_close(pixel_values, torch.from_numpy(expected))
|
|
|
|
|
|
def test_openvla_processor_outputs_pixel_values() -> None:
|
|
processor = OpenVLAProcessor(
|
|
image_processor=OpenVLAImageProcessor(image_size=224),
|
|
tokenizer=_FakeTokenizer(),
|
|
)
|
|
image = Image.new("RGB", (8, 8), color=(255, 0, 0))
|
|
|
|
batch = processor(
|
|
text="In: test\nOut:",
|
|
images=image,
|
|
text_kwargs={"add_special_tokens": True},
|
|
)
|
|
|
|
assert batch["input_ids"] == [[1, 10, 11]]
|
|
assert batch["pixel_values"].shape == (1, 6, 224, 224)
|
|
assert batch["pixel_values"].dtype == torch.float32
|
|
|
|
|
|
def test_openvla_image_processor_outputs_pixel_values() -> None:
|
|
processor = OpenVLAImageProcessor(image_size=224)
|
|
image = Image.new("RGB", (8, 8), color=(255, 0, 0))
|
|
|
|
output = processor(images=image)
|
|
|
|
assert output["pixel_values"].shape == (1, 6, 224, 224)
|
|
assert output["pixel_values"].dtype == torch.float32
|
|
|
|
|
|
def test_openvla_processing_info_token_counts() -> None:
|
|
info = _FakeOpenVLAProcessingInfo.__new__(_FakeOpenVLAProcessingInfo)
|
|
|
|
assert info.get_supported_mm_limits() == {"image": 1}
|
|
assert info.get_num_image_tokens(image_width=640, image_height=480) == 256
|
|
assert info.get_image_size_with_most_features().width == 224
|
|
assert info.get_image_size_with_most_features().height == 224
|
|
assert info.get_mm_max_tokens_per_item(seq_len=2048, mm_counts={"image": 1}) == {
|
|
"image": 256
|
|
}
|
|
|
|
|
|
def test_openvla_prompt_update_inserts_image_tokens_after_bos() -> None:
|
|
processor = _make_processor()
|
|
image = Image.new("RGB", (640, 480), color=(255, 255, 255))
|
|
mm_items = MultiModalDataItems({"image": ImageProcessorItems([image])})
|
|
|
|
assert (
|
|
processor._hf_processor_applies_updates("In: test\nOut:", mm_items, {}, {})
|
|
is False
|
|
)
|
|
|
|
prompt_update = processor._get_prompt_updates(mm_items, {}, {})[0]
|
|
resolved = prompt_update.resolve(0)
|
|
content = resolved.content
|
|
|
|
assert resolved.modality == "image"
|
|
assert [
|
|
(match.start_idx, match.end_idx)
|
|
for match in resolved.iter_matches([1, 10, 11], _FakeTokenizer())
|
|
] == [(1, 1)]
|
|
assert content.full == [32000] * 256
|
|
|
|
is_embed = content.is_embed(None, content.full)
|
|
assert is_embed.dtype == torch.bool
|
|
assert is_embed.tolist() == [True] * 256
|
|
|
|
|
|
def test_openvla_placeholder_string() -> None:
|
|
assert OpenVLAForActionPrediction.get_placeholder_str("image", 0) is None
|