[Test] Unify L2/L3 test layout, Buildkite steps, and test helpers (#2556)
Signed-off-by: wangyu <410167048@qq.com> Signed-off-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com> Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com> Co-authored-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
This commit is contained in:
1 parent
b85833eeec
commit
6f2ad7b403
134 files changed
+4904
-4185
No files matched your search
+434
-142
@@ -2,6 +2,7 @@
|
||||
|
||||
import base64
|
||||
import concurrent.futures
|
||||
import copy
|
||||
import errno
|
||||
import io
|
||||
import json
|
||||
@@ -13,7 +14,7 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import asdict, dataclass
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
@@ -23,7 +24,7 @@ import requests
|
||||
import soundfile as sf
|
||||
import torch
|
||||
import yaml
|
||||
from openai import OpenAI, omit
|
||||
from openai import APIError, OpenAI, omit
|
||||
from PIL import Image
|
||||
from vllm import TextPrompt
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
@@ -41,10 +42,48 @@ from tests.helpers.media import (
|
||||
decode_b64_image,
|
||||
)
|
||||
from vllm_omni.config.stage_config import resolve_deploy_yaml
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _split_request_config_by_per_output_sizes(cfg: dict[str, Any]) -> list[dict[str, Any]] | None:
|
||||
"""If ``extra_body`` has list ``height``/``width``, return one config per index (scalar h/w, ``num_outputs_per_prompt=1``)."""
|
||||
eb = cfg.get("extra_body")
|
||||
if not eb:
|
||||
return None
|
||||
h, w = eb.get("height"), eb.get("width")
|
||||
if (isinstance(h, (list, tuple)) or isinstance(w, (list, tuple))) and not (
|
||||
isinstance(h, (list, tuple)) and isinstance(w, (list, tuple))
|
||||
):
|
||||
raise ValueError("extra_body height and width must both be lists or both be scalars")
|
||||
if not (isinstance(h, (list, tuple)) and isinstance(w, (list, tuple))):
|
||||
return None
|
||||
if len(h) != len(w):
|
||||
raise ValueError(f"height and width lists must have equal length; got {len(h)=} {len(w)=}")
|
||||
n = len(h)
|
||||
n_out = eb.get("num_outputs_per_prompt")
|
||||
if n_out is not None:
|
||||
n_out = int(n_out)
|
||||
if n_out != n:
|
||||
raise ValueError(
|
||||
"When height/width are lists, num_outputs_per_prompt must equal their length; "
|
||||
f"got num_outputs_per_prompt={n_out}, len(lists)={n}"
|
||||
)
|
||||
splits: list[dict[str, Any]] = []
|
||||
for i in range(n):
|
||||
sub = copy.deepcopy(cfg)
|
||||
sub_eb = dict(sub.get("extra_body") or {})
|
||||
sub_eb["height"] = int(h[i])
|
||||
sub_eb["width"] = int(w[i])
|
||||
sub_eb["num_outputs_per_prompt"] = 1
|
||||
sub["extra_body"] = sub_eb
|
||||
splits.append(sub)
|
||||
return splits
|
||||
|
||||
|
||||
PromptAudioInput = list[tuple[Any, int]] | tuple[Any, int] | None
|
||||
PromptImageInput = list[Any] | Any | None
|
||||
PromptVideoInput = list[Any] | Any | None
|
||||
@@ -150,89 +189,6 @@ class OmniServerParams(NamedTuple):
|
||||
stage_init_timeout: int | None = None # None: fixture supplies default (600 s)
|
||||
|
||||
|
||||
def run_omni_server(
|
||||
request: Any,
|
||||
run_level: str,
|
||||
model_prefix: str,
|
||||
omni_fixture_lock: threading.Lock,
|
||||
) -> Generator["OmniServer", Any, None]:
|
||||
from tests.helpers.stage_config import modify_stage_config
|
||||
|
||||
with omni_fixture_lock:
|
||||
params: OmniServerParams = request.param
|
||||
model = model_prefix + params.model
|
||||
port = params.port
|
||||
stage_config_path = params.stage_config_path
|
||||
if run_level in {"advanced_model", "full_model"} and stage_config_path is not None:
|
||||
with open(stage_config_path, encoding="utf-8") as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
# Strip ``load_format: dummy`` (CI overlay default) so advanced_model
|
||||
# tests use real weights. New schema (``stages:``) writes the field
|
||||
# flat at stage level; legacy schema (``stage_args:``) nests it as
|
||||
# ``engine_args.load_format``. Handle both.
|
||||
new_schema_stages = cfg.get("stages")
|
||||
stage_key = "stages" if new_schema_stages is not None else "stage_args"
|
||||
delete_path = "load_format" if new_schema_stages is not None else "engine_args.load_format"
|
||||
stage_entries = cfg.get(stage_key, [])
|
||||
stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage]
|
||||
stage_config_path = modify_stage_config(
|
||||
stage_config_path,
|
||||
deletes={stage_key: {stage_id: [delete_path] for stage_id in stage_ids}},
|
||||
)
|
||||
|
||||
server_args = params.server_args or []
|
||||
if params.use_omni and params.stage_init_timeout is not None:
|
||||
server_args = [*server_args, "--stage-init-timeout", str(params.stage_init_timeout)]
|
||||
else:
|
||||
server_args = [*server_args, "--stage-init-timeout", "600"]
|
||||
if params.init_timeout is not None:
|
||||
server_args = [*server_args, "--init-timeout", str(params.init_timeout)]
|
||||
else:
|
||||
server_args = [*server_args, "--init-timeout", "900"]
|
||||
if params.use_stage_cli:
|
||||
if not params.use_omni:
|
||||
raise ValueError("omni_server with use_stage_cli=True requires use_omni=True")
|
||||
if stage_config_path is None:
|
||||
raise ValueError("omni_server with use_stage_cli=True requires a stage_config_path")
|
||||
server_args += ["--stage-configs-path", stage_config_path]
|
||||
|
||||
with OmniServerStageCli(
|
||||
model,
|
||||
stage_config_path,
|
||||
server_args,
|
||||
port=port,
|
||||
env_dict=params.env_dict,
|
||||
) as server:
|
||||
print("OmniServer started successfully")
|
||||
yield server
|
||||
print("OmniServer stopping...")
|
||||
else:
|
||||
if stage_config_path is not None:
|
||||
server_args += ["--stage-configs-path", stage_config_path]
|
||||
|
||||
with (
|
||||
OmniServer(
|
||||
model,
|
||||
server_args,
|
||||
port=port,
|
||||
env_dict=params.env_dict,
|
||||
use_omni=params.use_omni,
|
||||
)
|
||||
if port
|
||||
else OmniServer(
|
||||
model,
|
||||
server_args,
|
||||
env_dict=params.env_dict,
|
||||
use_omni=params.use_omni,
|
||||
)
|
||||
) as server:
|
||||
print("OmniServer started successfully")
|
||||
yield server
|
||||
print("OmniServer stopping...")
|
||||
|
||||
print("OmniServer stopped")
|
||||
|
||||
|
||||
class OmniServer:
|
||||
"""Omniserver for vLLM-Omni tests."""
|
||||
|
||||
@@ -653,6 +609,8 @@ class OmniResponse:
|
||||
error_message: str | None = None
|
||||
prompt_tokens: int | None = None
|
||||
cached_tokens: int | None = None
|
||||
# Set for HTTP-level assertions (e.g. ``assert_audio_speech_response`` with ``status_code`` in config).
|
||||
status_code: int | None = None
|
||||
logprobs: list | None = None
|
||||
|
||||
|
||||
@@ -667,6 +625,20 @@ class DiffusionResponse:
|
||||
error_message: str | None = None
|
||||
|
||||
|
||||
def _merge_diffusion_responses(parts: list[DiffusionResponse]) -> DiffusionResponse:
|
||||
"""Concatenate images in order; ``e2e_latency`` is wall-clock of the batch (set by caller) or max of parts."""
|
||||
merged = DiffusionResponse()
|
||||
merged.success = all(p.success for p in parts) and len(parts) > 0
|
||||
imgs: list[Image.Image] = []
|
||||
for p in parts:
|
||||
if p.images:
|
||||
imgs.extend(p.images)
|
||||
merged.images = imgs if imgs else None
|
||||
latencies = [p.e2e_latency for p in parts if p.e2e_latency is not None]
|
||||
merged.e2e_latency = max(latencies) if latencies else None
|
||||
return merged
|
||||
|
||||
|
||||
class OpenAIClientHandler:
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = None, api_key: str = "EMPTY", run_level: str = None):
|
||||
if port is None:
|
||||
@@ -946,6 +918,12 @@ class OpenAIClientHandler:
|
||||
- input: text to synthesize (required)
|
||||
- response_format: audio format such as "wav" or "pcm" (optional)
|
||||
- task_type, ref_text, ref_audio: TTS-specific extras (optional, passed via extra_body)
|
||||
- min_audio_bytes: optional minimum ``len(audio_bytes)`` checked in ``assert_audio_speech_response``
|
||||
- status_code: if set, HTTP status is asserted (int or e.g. ``(400, 422)``); uses APIError handling
|
||||
- err_message: optional substring(s) to match against error text (``str`` or list/tuple of alternatives;
|
||||
see ``assert_audio_speech_response``). If set, uses the same APIError path as ``status_code``.
|
||||
- When both ``status_code`` and ``err_message`` are absent (or each is ``None``), the normal request path
|
||||
is used (no try/except around ``APIError``).
|
||||
- timeout: request timeout in seconds (float, optional, default 120.0)
|
||||
- stream: whether to use streaming API (bool, optional, default False)
|
||||
"""
|
||||
@@ -970,8 +948,56 @@ class OpenAIClientHandler:
|
||||
|
||||
speech_fmt: str | None = None if response_format is omit else str(response_format).lower()
|
||||
|
||||
print(f"[audio.speech] start model={model}, stream={stream}, request_num={request_num}, timeout={timeout:.1f}s")
|
||||
|
||||
# Error validation path: only when at least one of these is set to a non-``None`` value.
|
||||
expect_error_handling = (request_config.get("status_code") is not None) or (
|
||||
request_config.get("err_message") is not None
|
||||
)
|
||||
if expect_error_handling and request_num != 1:
|
||||
raise ValueError(
|
||||
"request_config error validation (status_code / err_message) is only supported when request_num=1"
|
||||
)
|
||||
|
||||
if request_num == 1:
|
||||
if stream:
|
||||
req_start = time.perf_counter()
|
||||
if expect_error_handling:
|
||||
# ``status`` and/or ``err_message`` requested: catch APIError (4xx) and (optionally) assert body text;
|
||||
# HTTP 200 with JSON error body is handled in ``assert_audio_speech_response``.
|
||||
try:
|
||||
if stream:
|
||||
with self.client.audio.speech.with_streaming_response.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
) as resp:
|
||||
omni_resp = self._process_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
else:
|
||||
resp = self.client.audio.speech.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
)
|
||||
omni_resp = self._process_non_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
except APIError as e:
|
||||
sc = getattr(e, "status_code", None)
|
||||
if sc is None:
|
||||
raise
|
||||
omni_resp = OmniResponse(
|
||||
success=False,
|
||||
status_code=sc,
|
||||
error_message=str(e),
|
||||
)
|
||||
else:
|
||||
if getattr(omni_resp, "status_code", None) is None:
|
||||
omni_resp.status_code = 200
|
||||
elif stream:
|
||||
# Use streaming response helper.
|
||||
with self.client.audio.speech.with_streaming_response.create(
|
||||
model=model,
|
||||
@@ -995,6 +1021,8 @@ class OpenAIClientHandler:
|
||||
omni_resp = self._process_non_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
|
||||
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
||||
elapsed = time.perf_counter() - req_start
|
||||
print(f"[audio.speech] request#1 success in {elapsed:.3f}s")
|
||||
responses.append(omni_resp)
|
||||
return responses
|
||||
else:
|
||||
@@ -1002,7 +1030,8 @@ class OpenAIClientHandler:
|
||||
|
||||
if stream:
|
||||
|
||||
def _stream_task():
|
||||
def _stream_task(request_idx: int):
|
||||
task_start = time.perf_counter()
|
||||
with self.client.audio.speech.with_streaming_response.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
@@ -1011,83 +1040,137 @@ class OpenAIClientHandler:
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
) as resp:
|
||||
return self._process_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
result = self._process_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
elapsed = time.perf_counter() - task_start
|
||||
print(f"[audio.speech] request#{request_idx} success in {elapsed:.3f}s")
|
||||
return result
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = [executor.submit(_stream_task) for _ in range(request_num)]
|
||||
futures = {executor.submit(_stream_task, i + 1): i + 1 for i in range(request_num)}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
omni_resp = future.result()
|
||||
request_idx = futures[future]
|
||||
try:
|
||||
omni_resp = future.result()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[audio.speech] request#{request_idx} failed "
|
||||
f"(stream={stream}, timeout={timeout:.1f}s): {e!r}"
|
||||
)
|
||||
raise
|
||||
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
||||
responses.append(omni_resp)
|
||||
else:
|
||||
|
||||
def _non_stream_task(request_idx: int):
|
||||
task_start = time.perf_counter()
|
||||
resp = self.client.audio.speech.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
)
|
||||
elapsed = time.perf_counter() - task_start
|
||||
print(f"[audio.speech] request#{request_idx} success in {elapsed:.3f}s")
|
||||
return resp
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = []
|
||||
for _ in range(request_num):
|
||||
future = executor.submit(
|
||||
self.client.audio.speech.create,
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
)
|
||||
futures.append(future)
|
||||
futures = {executor.submit(_non_stream_task, i + 1): i + 1 for i in range(request_num)}
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
resp = future.result()
|
||||
request_idx = futures[future]
|
||||
try:
|
||||
resp = future.result()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[audio.speech] request#{request_idx} failed "
|
||||
f"(stream={stream}, timeout={timeout:.1f}s): {e!r}"
|
||||
)
|
||||
raise
|
||||
omni_resp = self._process_non_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
||||
responses.append(omni_resp)
|
||||
|
||||
return responses
|
||||
|
||||
def send_diffusion_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[DiffusionResponse]:
|
||||
def send_diffusion_request(
|
||||
self, request_config: dict[str, Any] | list[dict[str, Any]], request_num: int = 1
|
||||
) -> list[DiffusionResponse]:
|
||||
"""
|
||||
Send OpenAI requests for diffusion models.
|
||||
If ``extra_body`` has list ``height``/``width``, sends one chat completion per index in parallel
|
||||
(scalar h/w, ``num_outputs_per_prompt=1`` each) and merges images in list order.
|
||||
Args:
|
||||
request_config: Request configuration dictionary containing parameters like model, messages
|
||||
request_config: A single request configuration dict, or a list of
|
||||
request configuration dicts (one request per element)
|
||||
request_num: Number of requests to send concurrently, defaults to 1 (single request)
|
||||
Returns:
|
||||
list[DiffusionResponse]: List of DiffusionResponse objects containing the response data
|
||||
"""
|
||||
responses: list[DiffusionResponse] = []
|
||||
stream = request_config.get("stream", False)
|
||||
modalities = request_config.get("modalities", omit) # Most diffusion models don't require modalities param
|
||||
extra_body = request_config.get("extra_body", None)
|
||||
if stream:
|
||||
raise NotImplementedError("Streaming is not currently implemented for diffusion model e2e test")
|
||||
if request_num == 1:
|
||||
# Send single request
|
||||
chat_completion = self.client.chat.completions.create(
|
||||
model=request_config.get("model"),
|
||||
messages=request_config.get("messages"),
|
||||
extra_body=extra_body,
|
||||
|
||||
def _create_from_config(cfg: dict[str, Any]):
|
||||
stream = cfg.get("stream", False)
|
||||
if stream:
|
||||
raise NotImplementedError("Streaming is not currently implemented for diffusion model e2e test")
|
||||
modalities = cfg.get("modalities", omit) # Most diffusion models don't require modalities param
|
||||
eb = cfg.get("extra_body")
|
||||
extra = copy.deepcopy(eb) if eb else None
|
||||
return self.client.chat.completions.create(
|
||||
model=cfg.get("model"),
|
||||
messages=cfg.get("messages"),
|
||||
extra_body=extra,
|
||||
modalities=modalities,
|
||||
)
|
||||
|
||||
if isinstance(request_config, list):
|
||||
if not request_config:
|
||||
raise ValueError("request_config list must not be empty")
|
||||
if request_num != 1:
|
||||
raise ValueError("request_num is not supported when request_config is a list")
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(request_config)) as executor:
|
||||
futures = {executor.submit(_create_from_config, cfg): cfg for cfg in request_config}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
cfg = futures[future]
|
||||
chat_completion = future.result()
|
||||
response = self._process_diffusion_response(chat_completion)
|
||||
assert_diffusion_response(response, cfg, run_level=self.run_level)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
size_splits = _split_request_config_by_per_output_sizes(request_config)
|
||||
if size_splits is not None:
|
||||
if request_num != 1:
|
||||
raise ValueError(
|
||||
"request_num must be 1 when extra_body height/width are lists (split into concurrent per-size calls)"
|
||||
)
|
||||
t0 = time.perf_counter()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(size_splits)) as executor:
|
||||
futures = [executor.submit(_create_from_config, sub) for sub in size_splits]
|
||||
chat_completions = [f.result() for f in futures]
|
||||
parts = [self._process_diffusion_response(cc) for cc in chat_completions]
|
||||
merged = _merge_diffusion_responses(parts)
|
||||
merged.e2e_latency = time.perf_counter() - t0
|
||||
assert_diffusion_response(merged, request_config, run_level=self.run_level)
|
||||
return [merged]
|
||||
|
||||
if request_num == 1:
|
||||
# Send single request
|
||||
chat_completion = _create_from_config(request_config)
|
||||
response = self._process_diffusion_response(chat_completion)
|
||||
assert_diffusion_response(response, request_config, run_level=self.run_level)
|
||||
responses.append(response)
|
||||
else:
|
||||
# Send concurrent requests
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = []
|
||||
# Submit all request tasks
|
||||
for _ in range(request_num):
|
||||
future = executor.submit(
|
||||
self.client.chat.completions.create,
|
||||
model=request_config.get("model"),
|
||||
messages=request_config.get("messages"),
|
||||
modalities=modalities,
|
||||
extra_body=extra_body,
|
||||
)
|
||||
futures.append(future)
|
||||
# Process completed tasks
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
chat_completion = future.result()
|
||||
response = self._process_diffusion_response(chat_completion)
|
||||
assert_diffusion_response(response, request_config, run_level=self.run_level)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
# Send concurrent requests for the same request_config
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = [executor.submit(_create_from_config, request_config) for _ in range(request_num)]
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
chat_completion = future.result()
|
||||
response = self._process_diffusion_response(chat_completion)
|
||||
assert_diffusion_response(response, request_config, run_level=self.run_level)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
def send_video_diffusion_request(
|
||||
@@ -1380,7 +1463,7 @@ class OmniRunner:
|
||||
self,
|
||||
prompts: list[Any],
|
||||
sampling_params_list: list[Any] | None = None,
|
||||
) -> list[Any]:
|
||||
) -> list[OmniRequestOutput]:
|
||||
if sampling_params_list is None:
|
||||
sampling_params_list = self.get_default_sampling_params_list()
|
||||
return self.omni.generate(prompts, sampling_params_list)
|
||||
@@ -1395,7 +1478,7 @@ class OmniRunner:
|
||||
videos: PromptVideoInput = None,
|
||||
mm_processor_kwargs: dict[str, Any] | None = None,
|
||||
modalities: list[str] | None = None,
|
||||
) -> list[Any]:
|
||||
) -> list[OmniRequestOutput]:
|
||||
omni_inputs = self.get_omni_inputs(
|
||||
prompts=prompts,
|
||||
system_prompt=system_prompt,
|
||||
@@ -1460,10 +1543,10 @@ class OmniRunner:
|
||||
|
||||
|
||||
class OmniRunnerHandler:
|
||||
def __init__(self, omni_runner):
|
||||
def __init__(self, omni_runner: OmniRunner):
|
||||
self.runner = omni_runner
|
||||
|
||||
def _process_output(self, outputs: list[Any]) -> OmniResponse:
|
||||
def _process_omni_output(self, outputs: list[OmniRequestOutput]) -> OmniResponse:
|
||||
result = OmniResponse()
|
||||
try:
|
||||
text_content = None
|
||||
@@ -1482,7 +1565,20 @@ class OmniRunnerHandler:
|
||||
print(f"Error: {result.error_message}")
|
||||
return result
|
||||
|
||||
def send_request(self, request_config: dict[str, Any] | None = None) -> OmniResponse:
|
||||
def _process_diffusion_output(self, outputs: list[OmniRequestOutput]) -> DiffusionResponse:
|
||||
result = DiffusionResponse()
|
||||
output = outputs[0]
|
||||
if isinstance(output.images[0], list):
|
||||
# Returning frames of images as a video
|
||||
result.videos = output.images
|
||||
else:
|
||||
# Returning actual images
|
||||
result.images = output.images
|
||||
# [TODO] Add audio processing when tests are introduced
|
||||
result.success = True
|
||||
return result
|
||||
|
||||
def send_omni_request(self, request_config: dict[str, Any] | None = None) -> OmniResponse:
|
||||
if request_config is None:
|
||||
request_config = {}
|
||||
prompts = request_config.get("prompts")
|
||||
@@ -1493,10 +1589,71 @@ class OmniRunnerHandler:
|
||||
outputs = self.runner.generate_multimodal(
|
||||
prompts=prompts, videos=videos, images=images, audios=audios, modalities=modalities
|
||||
)
|
||||
response = self._process_output(outputs)
|
||||
response = self._process_omni_output(outputs)
|
||||
assert_omni_response(response, request_config, run_level="core_model")
|
||||
return response
|
||||
|
||||
def send_diffusion_request(self, request_config: dict[str, Any]) -> DiffusionResponse:
|
||||
prompt = request_config.get("prompt")
|
||||
if prompt is None:
|
||||
prompts = request_config.get("prompts")
|
||||
if not prompts:
|
||||
raise ValueError("request_config must contain a prompt")
|
||||
if len(prompts) > 1:
|
||||
raise ValueError(
|
||||
"In the current internal data structure, "
|
||||
"only one prompt is supported for diffusion requests. "
|
||||
"Because one prompt can contain multiple images or videos, "
|
||||
"the current internal data structure is ambiguous when multiple prompts are provided."
|
||||
)
|
||||
prompt = prompts[0]
|
||||
|
||||
# Sync previous `extra_body` field with sampling params object
|
||||
sampling_params: OmniDiffusionSamplingParams | None = request_config.get("sampling_params")
|
||||
if sampling_params is None:
|
||||
extra_body = request_config.get("extra_body", {})
|
||||
if extra_body:
|
||||
sampling_params = OmniDiffusionSamplingParams(**extra_body)
|
||||
else:
|
||||
extra_body = asdict(sampling_params)
|
||||
request_config["extra_body"] = extra_body
|
||||
if not extra_body:
|
||||
logger.warning("No sampling params provided in request_config, will skip output assertion")
|
||||
|
||||
negative_prompt = extra_body.get("negative_prompt") or request_config.get("negative_prompt")
|
||||
videos = request_config.get("videos")
|
||||
images = request_config.get("images")
|
||||
audios = request_config.get("audios")
|
||||
# Full dict (e.g. image + mask_image for inpainting) or partial; merged with top-level image/video keys.
|
||||
extra_multi_modal = request_config.get("multi_modal_data")
|
||||
modalities = request_config.get("modalities") # only used by limited models. Do not add default value here
|
||||
|
||||
prompt_object = OmniTextPrompt(prompt=prompt)
|
||||
if negative_prompt:
|
||||
prompt_object["negative_prompt"] = negative_prompt
|
||||
multi_modal: dict = {}
|
||||
if extra_multi_modal is not None:
|
||||
multi_modal.update(dict(extra_multi_modal))
|
||||
if videos is not None:
|
||||
multi_modal["video"] = videos
|
||||
if images is not None:
|
||||
multi_modal["image"] = images
|
||||
if audios is not None:
|
||||
multi_modal["audio"] = audios
|
||||
if multi_modal:
|
||||
prompt_object["multi_modal_data"] = multi_modal
|
||||
if modalities:
|
||||
prompt_object["modalities"] = modalities # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
start_time = time.perf_counter()
|
||||
response = self.runner.generate([prompt_object], [sampling_params] if sampling_params else None)
|
||||
end_time = time.perf_counter()
|
||||
|
||||
response = self._process_diffusion_output(response)
|
||||
response.e2e_latency = end_time - start_time
|
||||
assert_diffusion_response(response, request_config, run_level="core_model")
|
||||
return response
|
||||
|
||||
def send_audio_speech_request(self, request_config: dict[str, Any]) -> OmniResponse:
|
||||
"""
|
||||
Offline TTS: text -> audio via generate_multimodal, then validate with assert_audio_speech_response.
|
||||
@@ -1574,6 +1731,141 @@ class OmniRunnerHandler:
|
||||
return self.runner.stop_profile(stages=stages)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pytest fixture helpers (used from ``tests.helpers.fixtures.runtime``; live here
|
||||
# to avoid importing ``tests.helpers.runtime`` from the plugin module at import time).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _core_model_stage_config_path_with_dummy_load_format(stage_config_path: str | None, run_level: str) -> str | None:
|
||||
"""For ``core_model`` runs, patch every stage in the deploy YAML to ``load_format: dummy``."""
|
||||
if run_level != "core_model" or stage_config_path is None:
|
||||
return stage_config_path
|
||||
from tests.helpers.stage_config import modify_stage_config
|
||||
|
||||
with open(stage_config_path, encoding="utf-8") as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
new_schema_stages = cfg.get("stages")
|
||||
stage_key = "stages" if new_schema_stages is not None else "stage_args"
|
||||
update_path = "load_format" if new_schema_stages is not None else "engine_args.load_format"
|
||||
stage_entries = cfg.get(stage_key, [])
|
||||
stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage]
|
||||
return modify_stage_config(
|
||||
stage_config_path,
|
||||
updates={stage_key: {stage_id: {update_path: "dummy"} for stage_id in stage_ids}},
|
||||
)
|
||||
|
||||
|
||||
def iter_omni_server(
|
||||
request: Any,
|
||||
run_level: str,
|
||||
model_prefix: str,
|
||||
omni_fixture_lock: threading.Lock,
|
||||
) -> Generator[Any, Any, None]:
|
||||
"""Start/stop an Omni HTTP server; used by ``omni_server`` / ``omni_server_function`` fixtures."""
|
||||
from tests.helpers.stage_config import modify_stage_config
|
||||
|
||||
with omni_fixture_lock:
|
||||
params: OmniServerParams = request.param
|
||||
model = model_prefix + params.model
|
||||
port = params.port
|
||||
stage_config_path = params.stage_config_path
|
||||
if run_level in {"advanced_model", "full_model"} and stage_config_path is not None:
|
||||
with open(stage_config_path, encoding="utf-8") as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
new_schema_stages = cfg.get("stages")
|
||||
stage_key = "stages" if new_schema_stages is not None else "stage_args"
|
||||
delete_path = "load_format" if new_schema_stages is not None else "engine_args.load_format"
|
||||
stage_entries = cfg.get(stage_key, [])
|
||||
stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage]
|
||||
stage_config_path = modify_stage_config(
|
||||
stage_config_path,
|
||||
deletes={stage_key: {stage_id: [delete_path] for stage_id in stage_ids}},
|
||||
)
|
||||
|
||||
server_args = params.server_args or []
|
||||
if params.use_omni and params.stage_init_timeout is not None:
|
||||
server_args = [*server_args, "--stage-init-timeout", str(params.stage_init_timeout)]
|
||||
else:
|
||||
server_args = [*server_args, "--stage-init-timeout", "600"]
|
||||
if params.init_timeout is not None:
|
||||
server_args = [*server_args, "--init-timeout", str(params.init_timeout)]
|
||||
else:
|
||||
server_args = [*server_args, "--init-timeout", "900"]
|
||||
if params.use_stage_cli:
|
||||
if not params.use_omni:
|
||||
raise ValueError("omni_server with use_stage_cli=True requires use_omni=True")
|
||||
if stage_config_path is None:
|
||||
raise ValueError("omni_server with use_stage_cli=True requires a stage_config_path")
|
||||
server_args += ["--stage-configs-path", stage_config_path]
|
||||
|
||||
with OmniServerStageCli(
|
||||
model,
|
||||
stage_config_path,
|
||||
server_args,
|
||||
port=port,
|
||||
env_dict=params.env_dict,
|
||||
) as server:
|
||||
print("OmniServer started successfully")
|
||||
yield server
|
||||
print("OmniServer stopping...")
|
||||
else:
|
||||
if stage_config_path is not None:
|
||||
server_args += ["--stage-configs-path", stage_config_path]
|
||||
|
||||
with (
|
||||
OmniServer(
|
||||
model,
|
||||
server_args,
|
||||
port=port,
|
||||
env_dict=params.env_dict,
|
||||
use_omni=params.use_omni,
|
||||
)
|
||||
if port
|
||||
else OmniServer(
|
||||
model,
|
||||
server_args,
|
||||
env_dict=params.env_dict,
|
||||
use_omni=params.use_omni,
|
||||
)
|
||||
) as server:
|
||||
print("OmniServer started successfully")
|
||||
yield server
|
||||
print("OmniServer stopping...")
|
||||
|
||||
print("OmniServer stopped")
|
||||
|
||||
|
||||
def iter_omni_runner(
|
||||
request: Any,
|
||||
model_prefix: str,
|
||||
run_level: str,
|
||||
omni_fixture_lock: threading.Lock,
|
||||
) -> Generator[Any, None, None]:
|
||||
"""Yield an :class:`OmniRunner`; used by ``omni_runner`` / ``omni_runner_function`` fixtures."""
|
||||
with omni_fixture_lock:
|
||||
param = request.param
|
||||
if not isinstance(param, (tuple, list)) or len(param) not in (2, 3):
|
||||
raise ValueError(
|
||||
"omni_runner param must be (model, stage_config_path) or "
|
||||
"(model, stage_config_path, extra_omni_kwargs_dict)"
|
||||
)
|
||||
if len(param) == 2:
|
||||
model, stage_config_path = param[0], param[1]
|
||||
extra_omni_kwargs: dict = {}
|
||||
else:
|
||||
model, stage_config_path, extra = param[0], param[1], param[2]
|
||||
extra_omni_kwargs = dict(extra) if extra is not None else {}
|
||||
stage_config_path = _core_model_stage_config_path_with_dummy_load_format(stage_config_path, run_level)
|
||||
model = model_prefix + model
|
||||
with OmniRunner(model, seed=42, stage_configs_path=stage_config_path, **extra_omni_kwargs) as runner:
|
||||
print("OmniRunner started successfully")
|
||||
yield runner
|
||||
print("OmniRunner stopping...")
|
||||
|
||||
print("OmniRunner stopped")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DiffusionResponse",
|
||||
"OmniResponse",
|
||||
|
||||
Reference in new issue
Block a user