Files
ray-project--ray/python/ray/llm/tests/conftest.py
T
2026-07-13 13:17:40 +08:00

296 lines
8.8 KiB
Python

import base64
import io
import os
import tempfile
from typing import Generator, List
import PIL.Image
import pytest
import requests
from ray import serve
from ray.serve.llm import LLMConfig, ModelLoadingConfig, build_llm_deployment
S3_ARTIFACT_URL = "https://air-example-data.s3.amazonaws.com/"
S3_ARTIFACT_LLM_OSSCI_URL = S3_ARTIFACT_URL + "rayllm-ossci/"
S3_ARTIFACT_ASSETS_URL = S3_ARTIFACT_LLM_OSSCI_URL + "assets/"
def download_model_from_s3(
remote_url: str, file_list: List[str]
) -> Generator[str, None, None]:
"""
Download the model checkpoint and tokenizer from S3 for testing
The reason to download the model from S3 is to avoid downloading the model
from HuggingFace hub during testing, which is flaky because of the rate
limit and HF hub downtime.
Args:
remote_url: The remote URL to download the model from.
file_list: The list of files to download.
Yields:
str: The path to the downloaded model checkpoint and tokenizer.
"""
with tempfile.TemporaryDirectory(prefix="ray-llm-test-model") as checkpoint_dir:
print(f"Downloading model from {remote_url} to {checkpoint_dir}", flush=True)
for file_name in file_list:
response = requests.get(remote_url + file_name)
with open(os.path.join(checkpoint_dir, file_name), "wb") as fp:
fp.write(response.content)
yield os.path.abspath(checkpoint_dir)
@pytest.fixture(scope="session")
def model_opt_125m():
"""The small decoder model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_URL}facebook-opt-125m/"
FILE_LIST = [
"config.json",
"flax_model.msgpack",
"generation_config.json",
"merges.txt",
"pytorch_model.bin",
"special_tokens_map.json",
"tokenizer_config.json",
"vocab.json",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_llava_354m():
"""The vision language model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_URL}llava-354M/"
FILE_LIST = [
"added_tokens.json",
"chat_template.json",
"config.json",
"generation_config.json",
"model.safetensors",
"preprocessor_config.json",
"processor_config.json",
"special_tokens_map.json",
"tokenizer.json",
"tokenizer.model",
"tokenizer_config.json",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_smolvlm_256m():
"""The vision language model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_LLM_OSSCI_URL}smolvlm-256m-instruct/"
FILE_LIST = [
"added_tokens.json",
"chat_template.json",
"config.json",
"generation_config.json",
"merges.txt",
"model.safetensors",
"preprocessor_config.json",
"processor_config.json",
"special_tokens_map.json",
"tokenizer.json",
"tokenizer_config.json",
"vocab.json",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_llama_3_2_216M():
"""The llama 3.2 216M model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_URL}llama-3.2-216M-dummy/"
FILE_LIST = [
"config.json",
"generation_config.json",
"special_tokens_map.json",
"tokenizer_config.json",
"tokenizer.json",
"model.safetensors",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_llama_3_2_216M_lora():
"""The LoRA model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_URL}llama-3.2-216M-lora-dummy/"
FILE_LIST = [
"adapter_config.json",
"adapter_model.safetensors",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_pixtral_12b():
"""The Pixtral 12B model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_URL}mistral-community-pixtral-12b/"
FILE_LIST = [
"config.json",
"chat_template.json",
"preprocessor_config.json",
"processor_config.json",
"special_tokens_map.json",
"tokenizer_config.json",
"tokenizer.json",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_llama_3_2_1B_instruct():
"""The llama 3.2 1B Instruct model for testing."""
REMOTE_URL = f"{S3_ARTIFACT_URL}unsloth-Llama-3.2-1B-Instruct/"
FILE_LIST = [
"config.json",
"generation_config.json",
"model.safetensors",
"special_tokens_map.json",
"tokenizer_config.json",
"tokenizer.json",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def model_internlm2_1_8b():
"""
Yields the S3 URI so that download_model_files exercises the cloud download
path where the TOKENIZER_ONLY vs. EXCLUDE_SAFETENSORS filtering applies.
"""
yield "s3://anonymous@air-example-data/rayllm-ossci/internlm2-1_8b/"
@pytest.fixture(scope="session")
def model_qwen_2_5_omni_3b():
REMOTE_URL = f"{S3_ARTIFACT_LLM_OSSCI_URL}Qwen2.5-Omni-3B/"
FILE_LIST = [
"added_tokens.json",
"config.json",
"chat_template.json",
"generation_config.json",
"merges.txt",
"model-00001-of-00003.safetensors",
"model-00002-of-00003.safetensors",
"model-00003-of-00003.safetensors",
"model.safetensors.index.json",
"preprocessor_config.json",
"special_tokens_map.json",
"spk_dict.json",
"tokenizer.json",
"tokenizer_config.json",
"vocab.json",
]
yield from download_model_from_s3(REMOTE_URL, FILE_LIST)
@pytest.fixture(scope="session")
def gpu_type():
"""Get the GPU type used for testing."""
try:
import torch
print(f"{torch.version.cuda=}", flush=True)
name = torch.cuda.get_device_name()
# The name of the GPU is in the format of "NVIDIA L4" or "Tesla T4"
# or "NVIDIA H100 80GB HBM3"
type_name = name.split(" ")[1]
print(f"GPU type: {type_name}", flush=True)
yield type_name
except ImportError:
print("Failed to import torch to get GPU type", flush=True)
except ValueError as err:
print(f"Failed to get the GPU type: {err}", flush=True)
@pytest.fixture
def serve_cleanup():
yield
serve.shutdown()
@pytest.fixture
def create_model_opt_125m_deployment(gpu_type, model_opt_125m, serve_cleanup):
"""Create a serve deployment for testing."""
app_name = "test_serve_deployment_processor_app"
deployment_name = "test_deployment_name"
chat_template = """
{% if messages[0]['role'] == 'system' %}
{% set offset = 1 %}
{% else %}
{% set offset = 0 %}
{% endif %}
{{ bos_token }}
{% for message in messages %}
{% if (message['role'] == 'user') != (loop.index0 % 2 == offset) %}
{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}
{% endif %}
{{ '<|im_start|>' + message['role'] + '\n' + message['content'] | trim + '<|im_end|>\n' }}
{% endfor %}
{% if add_generation_prompt %}
{{ '<|im_start|>assistant\n' }}
{% endif %}
"""
# Create a vLLM serve deployment
llm_config = LLMConfig(
model_loading_config=ModelLoadingConfig(
model_id=model_opt_125m,
model_source=model_opt_125m,
),
accelerator_type=gpu_type,
deployment_config=dict(
name="test_deployment_name", # This is not necessarily the final deployment name
autoscaling_config=dict(
min_replicas=1,
max_replicas=1,
),
),
engine_kwargs=dict(
enable_prefix_caching=True,
enable_chunked_prefill=True,
max_num_batched_tokens=4096,
# Add chat template for OPT model to enable chat API
chat_template=chat_template,
),
)
llm_app = build_llm_deployment(
llm_config, override_serve_options=dict(name=deployment_name)
)
serve.run(llm_app, name=app_name)
yield deployment_name, app_name
@pytest.fixture
def image_asset():
image_url = S3_ARTIFACT_ASSETS_URL + "cherry_blossom.jpg"
with requests.get(image_url) as response:
response.raise_for_status()
image_pil = PIL.Image.open(io.BytesIO(response.content))
yield image_url, image_pil
@pytest.fixture
def audio_asset():
audio_url = S3_ARTIFACT_ASSETS_URL + "winning_call.ogg"
with requests.get(audio_url) as response:
response.raise_for_status()
audio_data = base64.b64encode(response.content).decode("utf-8")
yield audio_url, audio_data
@pytest.fixture
def video_asset():
video_url = S3_ARTIFACT_ASSETS_URL + "free-videos.mp4"
yield video_url