Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1459 lines
54 KiB
Python

"""
Config-driven diffusion generation test with pytest parametrization.
Each collected request prints a performance log before validation.
"""
from __future__ import annotations
import json
import os
import queue
import threading
import time
from pathlib import Path
from typing import Any, Callable
import numpy as np
import openai
import pytest
import requests
from openai import OpenAI
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.runtime.utils.perf_logger import RequestPerfRecord
from sglang.multimodal_gen.test.server import conftest
from sglang.multimodal_gen.test.server.realtime_consistency import (
pop_realtime_key_frames,
pop_realtime_perf_stats,
validate_realtime_perf_stats,
)
from sglang.multimodal_gen.test.server.test_server_utils import (
VALIDATOR_REGISTRY,
PerformanceValidator,
ServerContext,
ServerManager,
get_generate_fn,
)
from sglang.multimodal_gen.test.server.testcase_configs import (
BASELINE_CONFIG,
DiffusionTestCase,
PerformanceSummary,
ScenarioConfig,
get_model_task_type_for_server_args,
get_perf_baseline_path,
)
from sglang.multimodal_gen.test.test_utils import (
SGL_TEST_FILES_CI_DATA_REVISION,
_consistency_gt_filenames,
_get_consistency_gt_dir,
action_gt_exists,
compare_with_gt,
extract_key_frames_from_video,
get_action_consistency_gt_candidates,
get_action_consistency_gt_remote_files,
get_consistency_gt_candidates,
get_consistency_gt_remote_files,
get_consistency_threshold_path,
get_consistency_thresholds,
get_dynamic_server_port,
gt_exists,
image_bytes_to_numpy,
load_action_consistency_gt,
load_consistency_gt,
save_consistency_failure_artifact,
wait_for_req_perf_record,
)
logger = init_logger(__name__)
# Track test cases missing estimated_full_test_time_s for time measurement output
_MISSING_ESTIMATED_TIME_CASES: set[str] = set()
_PENDING_BASELINE_DUMPS: dict[str, tuple[PerformanceSummary, bool]] = {}
_OPENAI_REQUEST_TIMEOUT_SECS = float(
os.environ.get("SGLANG_TEST_OPENAI_REQUEST_TIMEOUT_SECS", "600")
)
_SERVER_EXIT_POLL_INTERVAL_SECS = float(
os.environ.get("SGLANG_TEST_SERVER_EXIT_POLL_INTERVAL_SECS", "1")
)
_CONTROL_API_TIMEOUT_SECS = float(
os.environ.get("SGLANG_TEST_CONTROL_API_TIMEOUT_SECS", "300")
)
_SERVER_FATAL_LOG_PATTERNS = (
"terminate called after throwing an instance of",
"Fatal Python error:",
"Segmentation fault",
"Aborted (core dumped)",
)
_CASE_LOG_SEPARATOR = "=" * 88
def _print_case_log_separator(case_id: str, state: str) -> None:
print(
f"\n{_CASE_LOG_SEPARATOR}\n"
f"[server-test] {state}: {case_id}\n"
f"{_CASE_LOG_SEPARATOR}",
flush=True,
)
@pytest.fixture
def diffusion_server(case: DiffusionTestCase) -> ServerContext:
"""Start a diffusion server for a single case and tear it down afterwards."""
_fixture_start_time = time.perf_counter()
server_args = case.server_args
_print_case_log_separator(case.id, "BEGIN diffusion testcase")
# Skip ring attention tests on AMD/ROCm - Ring Attention requires Flash Attention
# which is not available on AMD. Use Ulysses parallelism instead.
if (
current_platform.is_hip()
and server_args.ring_degree is not None
and server_args.ring_degree > 1
):
pytest.skip(
f"Skipping {case.id}: Ring Attention (ring_degree={server_args.ring_degree}) "
"requires Flash Attention which is not available on AMD/ROCm"
)
default_port = get_dynamic_server_port()
port = int(os.environ.get("SGLANG_TEST_SERVER_PORT", default_port))
extra_args = os.environ.get("SGLANG_TEST_SERVE_ARGS", "")
extra_args = f"--model-type diffusion {extra_args}".strip()
extra_args += f" --num-gpus {server_args.num_gpus}"
if server_args.tp_size is not None:
extra_args += f" --tp-size {server_args.tp_size}"
if server_args.ulysses_degree is not None:
extra_args += f" --ulysses-degree {server_args.ulysses_degree}"
if server_args.dit_layerwise_offload:
extra_args += " --dit-layerwise-offload true"
if server_args.dit_offload_prefetch_size:
extra_args += (
f" --dit-offload-prefetch-size {server_args.dit_offload_prefetch_size}"
)
if server_args.text_encoder_cpu_offload:
extra_args += " --text-encoder-cpu-offload"
if server_args.ring_degree is not None:
extra_args += f" --ring-degree {server_args.ring_degree}"
if server_args.cfg_parallel:
extra_args += " --enable-cfg-parallel"
# LoRA support
if server_args.lora_path:
extra_args += f" --lora-path {server_args.lora_path}"
# Strict ports: fail immediately if port is occupied instead of silently
# picking another one (which causes the test client to connect to the wrong server).
extra_args += " --strict-ports"
# Shape-only mesh cases (e.g. hunyuan3d_shape_gen) validate geometry via
# mesh-correctness and must NOT run the paint/texture stages, whose
# verification checks texture artifacts (paint_mesh/normal_maps/renderer)
# that the shape-only path never produces. Inject a pipeline-config override
# disabling paint for these cases.
if server_args.custom_validator == "mesh":
import json as _json
import tempfile as _tempfile
_paint_off_cfg = os.path.join(
_tempfile.gettempdir(), f"{case.id}_paint_off.json"
)
with open(_paint_off_cfg, "w") as _f:
_json.dump({"paint_enable": False}, _f)
extra_args += f" --config {_paint_off_cfg}"
for arg in server_args.extras:
extra_args += f" {arg}"
# Build custom environment variables
env_vars = {}
if server_args.enable_cache_dit:
env_vars["SGLANG_CACHE_DIT_ENABLED"] = "true"
env_vars.update(server_args.env_vars)
# start server
wait_deadline = float(os.environ.get("SGLANG_TEST_WAIT_SECS", "1200"))
logger.info(
"[server-test] Starting server for test case: %s\n"
" Model: %s\n"
" Port: %s\n"
" Wait deadline: %ss\n"
" Extra args: %s\n"
" Num GPUs: %s",
case.id,
server_args.model_path,
port,
wait_deadline,
extra_args,
server_args.num_gpus,
)
manager = ServerManager(
model=server_args.model_path,
port=port,
wait_deadline=wait_deadline,
extra_args=extra_args,
env_vars=env_vars,
)
try:
ctx = manager.start()
except (RuntimeError, TimeoutError) as exc:
# Auto-skip when the installed diffusers version lacks the required
# pipeline class. This avoids hard failures when a model needs a
# newer diffusers release than what is currently installed in CI.
msg = str(exc)
if "not found in diffusers" in msg or (
"has no attribute" in msg and "diffusers" in msg.lower()
):
pytest.skip(
f"Skipping {case.id}: required diffusers pipeline class "
f"is not available in the installed version. "
f"Upgrade diffusers to enable this test."
)
_print_case_log_separator(case.id, "FAILED during server startup")
raise
try:
yield ctx
finally:
ctx.cleanup()
_fixture_end_time = time.perf_counter()
_measured_full_time = _fixture_end_time - _fixture_start_time
is_baseline_generation_mode = os.environ.get("SGLANG_GEN_BASELINE", "0") == "1"
pending_dump = _PENDING_BASELINE_DUMPS.pop(case.id, None)
if pending_dump is not None:
summary, missing_scenario = pending_dump
DiffusionServerBase()._dump_baseline_for_testcase(
case,
summary,
missing_scenario=missing_scenario,
measured_full_time=_measured_full_time,
)
scenario = BASELINE_CONFIG.scenarios.get(case.id)
needs_estimated_time = (
scenario is None or scenario.estimated_full_test_time_s is None
)
if needs_estimated_time and not is_baseline_generation_mode:
_MISSING_ESTIMATED_TIME_CASES.add(case.id)
logger.error(
f'\n{"=" * 60}\n'
f'Add "estimated_full_test_time_s" to scenario "{case.id}":\n\n'
f"File: {get_perf_baseline_path()}\n\n"
f' "{case.id}": {{\n'
f" ...\n"
f' "estimated_full_test_time_s": {_measured_full_time:.1f}\n'
f" }}\n"
f'{"=" * 60}\n'
)
_print_case_log_separator(case.id, "END diffusion testcase")
class DiffusionServerBase:
"""Performance tests for all diffusion models/scenarios.
This single test class runs against all cases defined in ONE_GPU_CASES.
Each case gets its own server instance via the parametrized fixture.
"""
_perf_results: list[dict[str, Any]] = []
_pytest_config = None # Store pytest config for stash access
@classmethod
def setup_class(cls):
cls._perf_results = []
@classmethod
def teardown_class(cls):
print(
f"\n[DEBUG teardown_class] Called for {cls.__name__}, _perf_results has {len(cls._perf_results)} entries"
)
if cls._pytest_config:
# Add results to pytest stash (shared across all import contexts)
for result in cls._perf_results:
result["class_name"] = cls.__name__
conftest.add_perf_results(cls._pytest_config, cls._perf_results)
print(
f"[DEBUG teardown_class] Added {len(cls._perf_results)} results to stash"
)
else:
print(
"[DEBUG teardown_class] No pytest_config available, skipping stash update"
)
@pytest.fixture(autouse=True)
def _capture_pytest_config(self, request):
"""Capture pytest config for use in teardown_class."""
self.__class__._pytest_config = request.config
def _client(self, ctx: ServerContext) -> OpenAI:
"""Get OpenAI client for the server."""
return OpenAI(
api_key="sglang-anything",
base_url=f"http://localhost:{ctx.port}/v1",
timeout=_OPENAI_REQUEST_TIMEOUT_SECS,
max_retries=0,
)
def _fail_if_server_stopped_or_crashed(
self, ctx: ServerContext, case_id: str
) -> None:
returncode = ctx.process.poll()
if returncode is None:
tail = ctx.log_tail()
for pattern in _SERVER_FATAL_LOG_PATTERNS:
if pattern in tail:
pytest.fail(
f"{case_id}: server reported a fatal backend error during "
f"generation: {pattern}\n\nServer log tail:\n{tail}",
pytrace=False,
)
return
tail = ctx.log_tail()
message = (
f"{case_id}: server process exited during generation "
f"(code {returncode})."
)
if tail:
message += f"\n\nServer log tail:\n{tail}"
pytest.fail(message, pytrace=False)
def _run_generation_with_server_watchdog(
self,
ctx: ServerContext,
case_id: str,
generate_fn: Callable[[str, openai.Client], tuple[str, bytes]],
client: openai.Client,
) -> tuple[str, bytes]:
result_queue: queue.Queue[tuple[str, tuple[str, bytes] | BaseException]] = (
queue.Queue(maxsize=1)
)
def _target() -> None:
try:
result_queue.put(("ok", generate_fn(case_id, client)))
except BaseException as exc:
result_queue.put(("error", exc))
# native backend crashes can leave the HTTP client blocked until its read
# timeout; keep the request in a daemon thread so the main test thread can
# fail as soon as the server subprocess exits
thread = threading.Thread(
target=_target,
name=f"diffusion-generation-{case_id}",
daemon=True,
)
thread.start()
while True:
try:
state, payload = result_queue.get(
timeout=_SERVER_EXIT_POLL_INTERVAL_SECS
)
except queue.Empty:
self._fail_if_server_stopped_or_crashed(ctx, case_id)
continue
if state == "ok":
if isinstance(payload, BaseException):
raise payload
return payload
self._fail_if_server_stopped_or_crashed(ctx, case_id)
if not isinstance(payload, BaseException):
pytest.fail(f"{case_id}: invalid generation result state: {state}")
raise payload
def run_and_collect(
self,
ctx: ServerContext,
case_id: str,
generate_fn: Callable[[str, openai.Client], tuple[str, bytes]],
collect_perf: bool = True,
) -> tuple[RequestPerfRecord | None, bytes]:
"""Run generation and optionally collect performance records.
Returns:
Tuple of (performance_record, content_bytes)
"""
client = self._client(ctx)
rid, content = self._run_generation_with_server_watchdog(
ctx, case_id, generate_fn, client
)
if not collect_perf:
return None, content
log_path = ctx.perf_log_path
log_wait_timeout = 30
req_perf_record = wait_for_req_perf_record(
rid,
log_path,
timeout=log_wait_timeout,
)
return (req_perf_record, content)
def _validate_and_record(
self,
case: DiffusionTestCase,
perf_record: RequestPerfRecord,
) -> None:
"""Validate metrics and record results."""
is_baseline_generation_mode = os.environ.get("SGLANG_GEN_BASELINE", "0") == "1"
scenario = BASELINE_CONFIG.scenarios.get(case.id)
missing_scenario = False
if scenario is None:
# Create dummy scenario to allow metric collection
scenario = type(
"DummyScenario",
(),
{
"expected_e2e_ms": 0,
"expected_avg_denoise_ms": 0,
"expected_median_denoise_ms": 0,
"stages_ms": {},
"denoise_step_ms": {},
},
)()
if not is_baseline_generation_mode:
missing_scenario = True
if (
not missing_scenario
and not is_baseline_generation_mode
and scenario.estimated_full_test_time_s is None
):
_MISSING_ESTIMATED_TIME_CASES.add(case.id)
validator_name = case.server_args.custom_validator or "default"
validator_class = VALIDATOR_REGISTRY.get(validator_name, PerformanceValidator)
validator = validator_class(
scenario=scenario,
tolerances=BASELINE_CONFIG.tolerances,
step_fractions=BASELINE_CONFIG.step_fractions,
)
summary = validator.collect_metrics(perf_record)
self._print_performance_log(case, summary, scenario)
if case.run_perf_check:
if is_baseline_generation_mode:
_PENDING_BASELINE_DUMPS[case.id] = (summary, missing_scenario)
return
if missing_scenario:
self._dump_baseline_for_testcase(case, summary, missing_scenario)
if missing_scenario:
pytest.fail(
f"Testcase '{case.id}' not found in {get_perf_baseline_path()}"
)
return
# only run performance validation if run_perf_check is True
try:
validator.validate(perf_record, case.sampling_params.num_frames)
except AssertionError as e:
logger.error(f"Performance validation failed for {case.id}:\n{e}")
self._dump_baseline_for_testcase(case, summary, missing_scenario)
raise
result = {
"test_name": case.id,
"modality": case.server_args.modality,
"e2e_ms": summary.e2e_ms,
"avg_denoise_ms": summary.avg_denoise_ms,
"median_denoise_ms": summary.median_denoise_ms,
"stage_metrics": summary.stage_metrics,
"sampled_steps": summary.sampled_steps,
}
# video-specific metrics
if summary.frames_per_second:
result.update(
{
"frames_per_second": summary.frames_per_second,
"total_frames": summary.total_frames,
"avg_frame_time_ms": summary.avg_frame_time_ms,
}
)
self.__class__._perf_results.append(result)
print(
f"[DEBUG _validate_and_record] Appended result for {case.id}, class {self.__class__.__name__} now has {len(self.__class__._perf_results)} results"
)
def _print_performance_log(
self,
case: DiffusionTestCase,
summary: PerformanceSummary,
scenario: ScenarioConfig | None,
) -> None:
lines = [
"",
f"--- Performance Log: {case.id} ---",
(
f" e2e={summary.e2e_ms:.2f}ms, "
f"avg_denoise={summary.avg_denoise_ms:.2f}ms, "
f"median_denoise={summary.median_denoise_ms:.2f}ms"
),
]
if scenario is not None:
lines.append(
" baseline: "
f"e2e={scenario.expected_e2e_ms:.2f}ms, "
f"avg_denoise={scenario.expected_avg_denoise_ms:.2f}ms, "
f"median_denoise={scenario.expected_median_denoise_ms:.2f}ms"
)
if summary.stage_metrics:
stages = ", ".join(
f"{name}={duration:.2f}ms"
for name, duration in summary.stage_metrics.items()
)
lines.append(f" stages: {stages}")
if summary.all_denoise_steps:
# ci retries need the exact outlier, not only sampled checkpoints
steps = ", ".join(
f"{idx}={duration:.2f}ms"
for idx, duration in sorted(summary.all_denoise_steps.items())
)
lines.append(f" denoise_steps: {steps}")
lines.append(f"--- End Performance Log: {case.id} ---")
print("\n".join(lines), flush=True)
def _dump_baseline_for_testcase(
self,
case: DiffusionTestCase,
summary: PerformanceSummary,
missing_scenario: bool = False,
measured_full_time: float | None = None,
) -> None:
"""Dump performance metrics as a JSON scenario for baselines."""
import json
denoise_steps_formatted = {
str(k): round(v, 2) for k, v in summary.all_denoise_steps.items()
}
stages_formatted = {k: round(v, 2) for k, v in summary.stage_metrics.items()}
baseline = {
"stages_ms": stages_formatted,
"denoise_step_ms": denoise_steps_formatted,
"expected_e2e_ms": round(summary.e2e_ms, 2),
"expected_avg_denoise_ms": round(summary.avg_denoise_ms, 2),
"expected_median_denoise_ms": round(summary.median_denoise_ms, 2),
}
if measured_full_time is not None:
baseline["estimated_full_test_time_s"] = round(measured_full_time, 1)
# Video-specific metrics
if case.server_args.modality == "video":
if "per_frame_generation" not in baseline["stages_ms"]:
baseline["stages_ms"]["per_frame_generation"] = (
round(summary.avg_frame_time_ms, 2)
if summary.avg_frame_time_ms
else None
)
action = "add" if missing_scenario else "update"
output = f"""
{action} this baseline in the "scenarios" section of {get_perf_baseline_path()}:
"{case.id}": {json.dumps(baseline, indent=4)}
"""
logger.error(output)
def _validate_consistency(
self,
case: DiffusionTestCase,
content: bytes,
) -> None:
"""Validate output consistency against ground truth using CLIP similarity."""
if os.environ.get("SGLANG_SKIP_CONSISTENCY", "0") == "1":
logger.info(
f"[Consistency] Skipping consistency check for {case.id} (SGLANG_SKIP_CONSISTENCY=1)"
)
return
if not content:
logger.warning(
f"[Consistency] Skipping consistency check for {case.id}: "
"content is empty (generation may have timed out)"
)
return
if case.server_args.modality == "action":
self._validate_action_consistency(case, content)
return
num_gpus = case.server_args.num_gpus
is_video = case.server_args.modality == "video"
output_format = case.sampling_params.output_format
if not gt_exists(
case.id, num_gpus, is_video=is_video, output_format=output_format
):
if _get_consistency_gt_dir() is not None:
names = ", ".join(
get_consistency_gt_candidates(
case.id, num_gpus, is_video, output_format
)
)
else:
names = ", ".join(
_consistency_gt_filenames(
case.id, num_gpus, is_video, output_format
)
)
logger.error(f"""
--- MISSING GROUND TRUTH DETECTED ---
GT image(s) not found for '{case.id}'.
Add the expected file(s) to sgl-project/ci-data in diffusion-ci/consistency_gt/sglang_generated/ with naming (n=num_gpus).
Image: {case.id}_{{n}}gpu.<ext> (ext from output_format: png, jpg, webp)
Video: {case.id}_{{n}}gpu_frame_0.png, {case.id}_{{n}}gpu_frame_mid.png, {case.id}_{{n}}gpu_frame_last.png
For this case, expected file(s): {names}
Repository: https://github.com/sgl-project/ci-data (path: diffusion-ci/consistency_gt/sglang_generated/, with optional platform subdirectories such as 5090/)
Pinned revision used by this check: {SGL_TEST_FILES_CI_DATA_REVISION}
(Optional) Per-case override in {get_consistency_threshold_path()}:
"cases": {{
"{case.id}": {{
"clip_threshold": 0.92,
"ssim_threshold": 0.95,
"psnr_threshold": 28.0,
"mean_abs_diff_threshold": 8.0
}}
}}
""")
pytest.fail(
f"GT not found for {case.id}. See logs for instructions to add GT."
)
gt_data = load_consistency_gt(
case.id, num_gpus, is_video=is_video, output_format=output_format
)
thresholds = get_consistency_thresholds(case.id, is_video=is_video)
if is_video:
output_frames = pop_realtime_key_frames(case.id)
if output_frames is None:
output_frames = extract_key_frames_from_video(content)
else:
output_frames = [image_bytes_to_numpy(content)]
result = compare_with_gt(
output_frames=output_frames,
gt_data=gt_data,
thresholds=thresholds,
case_id=case.id,
)
if not result.passed:
failed_frames = []
gt_remote_files = get_consistency_gt_remote_files(
case.id,
num_gpus,
is_video=is_video,
output_format=output_format,
)
artifact_path = save_consistency_failure_artifact(
artifact_dir=os.environ.get("SGLANG_DIFFUSION_ARTIFACT_DIR"),
case_id=case.id,
num_gpus=num_gpus,
output_frames=output_frames,
gt_data=gt_data,
result=result,
is_video=is_video,
output_format=output_format,
gt_remote_files=gt_remote_files,
)
if artifact_path is not None:
logger.info(
"[Artifact] Saved consistency failure comparison: %s",
artifact_path,
)
gt_remote_info = "\n".join(
f" - {filename}: {url}" for filename, url in gt_remote_files
)
for metric in result.frame_metrics:
failed_metrics = []
if not metric.clip_passed:
failed_metrics.append("clip")
if not metric.ssim_passed:
failed_metrics.append("ssim")
if not metric.psnr_passed:
failed_metrics.append("psnr")
if not metric.mean_abs_diff_passed:
failed_metrics.append("mean_abs_diff")
if failed_metrics:
failed_frames.append(
f" - f{metric.frame_index} "
f"[{', '.join(failed_metrics)}] "
f"clip={metric.clip_similarity:.4f} "
f"ssim={metric.ssim:.4f} "
f"psnr={metric.psnr:.4f} "
f"mean_abs_diff={metric.mean_abs_diff:.4f}"
)
pytest.fail(
f"Consistency check failed for {case.id}:\n"
f" Metrics: sim={result.min_similarity:.4f}, "
f"ssim={result.min_ssim:.4f}, "
f"psnr={result.min_psnr:.4f}, "
f"mean_abs_diff={result.max_mean_abs_diff:.4f}\n"
f" Thresholds: clip>={result.thresholds.clip_threshold}, "
f"ssim>={result.thresholds.ssim_threshold}, "
f"psnr>={result.thresholds.psnr_threshold}, "
f"mean_abs_diff<={result.thresholds.mean_abs_diff_threshold}\n"
f" Failed frames:\n"
+ "\n".join(failed_frames)
+ f"\n Compared GT files and links:\n{gt_remote_info}"
)
logger.info(
f"[Consistency] {case.id}: PASSED "
f"(min_similarity={result.min_similarity:.4f}, "
f"min_ssim={result.min_ssim:.4f}, "
f"min_psnr={result.min_psnr:.4f}, "
f"max_mean_abs_diff={result.max_mean_abs_diff:.4f})"
)
def _extract_action_array(
self,
payload: dict[str, Any],
expected_horizon: int,
expected_dim: int,
) -> np.ndarray:
action = payload["data"][0]["action"]
values = action["values"]
assert action["shape"] == [expected_horizon, expected_dim]
array = np.asarray(values, dtype=np.float32)
assert array.shape == (expected_horizon, expected_dim)
assert np.isfinite(array).all()
return array
def _validate_action_consistency(
self,
case: DiffusionTestCase,
content: bytes,
) -> None:
payload = json.loads(content.decode("utf-8"))
expected_horizon = int(case.sampling_params.extras.get("action_horizon", 50))
expected_dim = int(case.sampling_params.extras.get("action_dim", 32))
output = self._extract_action_array(payload, expected_horizon, expected_dim)
num_gpus = case.server_args.num_gpus
if not action_gt_exists(case.id, num_gpus):
names = ", ".join(get_action_consistency_gt_candidates(case.id, num_gpus))
logger.error(f"""
--- MISSING ACTION GROUND TRUTH DETECTED ---
GT action JSON not found for '{case.id}'.
Add the expected file to sgl-project/ci-data in diffusion-ci/consistency_gt/sglang_generated/ with naming:
Action: {case.id}_{{n}}gpu.json
For this case, expected file(s): {names}
Repository: https://github.com/sgl-project/ci-data (path: diffusion-ci/consistency_gt/sglang_generated/, with optional platform subdirectories such as 5090/)
Pinned revision used by this check: {SGL_TEST_FILES_CI_DATA_REVISION}
""")
pytest.fail(
f"GT action JSON not found for {case.id}. See logs for instructions to add GT."
)
gt_payload = load_action_consistency_gt(case.id, num_gpus)
gt = self._extract_action_array(gt_payload, expected_horizon, expected_dim)
abs_diff = np.abs(output - gt)
max_abs_diff = float(abs_diff.max())
mean_abs_diff = float(abs_diff.mean())
max_abs_threshold = float(
case.sampling_params.extras.get("action_max_abs_diff_threshold", 0.05)
)
mean_abs_threshold = float(
case.sampling_params.extras.get("action_mean_abs_diff_threshold", 0.005)
)
if max_abs_diff > max_abs_threshold or mean_abs_diff > mean_abs_threshold:
gt_remote_info = "\n".join(
f" - {filename}: {url}"
for filename, url in get_action_consistency_gt_remote_files(
case.id,
num_gpus,
)
)
pytest.fail(
f"Action consistency check failed for {case.id}:\n"
f" max_abs_diff={max_abs_diff:.6f} "
f"(threshold {max_abs_threshold:.6f})\n"
f" mean_abs_diff={mean_abs_diff:.6f} "
f"(threshold {mean_abs_threshold:.6f})\n"
f" Compared GT files and links:\n{gt_remote_info}"
)
logger.info(
"[Consistency] %s: PASSED action GT check "
"(shape=%sx%s, max_abs_diff=%.6f, mean_abs_diff=%.6f)",
case.id,
expected_horizon,
expected_dim,
max_abs_diff,
mean_abs_diff,
)
def _save_gt_output(
self,
case: DiffusionTestCase,
content: bytes,
) -> None:
"""Save generated content as ground truth files.
Args:
case: Test case configuration
content: Generated content bytes (image or video)
"""
gt_output_dir = os.environ.get("SGLANG_GT_OUTPUT_DIR")
if not gt_output_dir:
logger.error("SGLANG_GT_OUTPUT_DIR not set, cannot save GT output")
return
out_dir = Path(gt_output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
num_gpus = case.server_args.num_gpus
is_video = case.server_args.modality == "video"
if case.server_args.modality == "action":
output_path = out_dir / f"{case.id}_{num_gpus}gpu.json"
output_path.write_bytes(content)
logger.info(f"Saved GT action JSON: {output_path}")
return
if is_video:
# realtime consistency uses websocket raw frames to avoid lossy mp4 drift
frames = pop_realtime_key_frames(case.id)
if frames is None:
frames = extract_key_frames_from_video(
content, num_frames=case.sampling_params.num_frames
)
if len(frames) != 3:
logger.warning(
f"{case.id}: expected 3 frames, got {len(frames)}, skipping frame save"
)
return
# Save frames (reuse naming from _consistency_gt_filenames)
filenames = _consistency_gt_filenames(case.id, num_gpus, is_video=True)
from PIL import Image
for frame, fn in zip(frames, filenames):
frame_path = out_dir / fn
Image.fromarray(frame).save(frame_path)
logger.info(f"Saved GT frame: {frame_path}")
else:
# Save image
from sglang.multimodal_gen.test.test_utils import detect_image_format
detected_format = detect_image_format(content)
filenames = _consistency_gt_filenames(
case.id, num_gpus, is_video=False, output_format=detected_format
)
output_path = out_dir / filenames[0]
output_path.write_bytes(content)
logger.info(f"Saved GT image: {output_path} (format: {detected_format})")
def _validate_lora_consistency(
self, case: DiffusionTestCase, content: bytes, operation: str
) -> None:
if not case.run_consistency_check:
logger.info(
"[LoRA Consistency] Skipping %s consistency for %s: disabled for case",
operation,
case.id,
)
return
logger.info(
"[LoRA Consistency] Validating %s output for %s", operation, case.id
)
self._validate_consistency(case, content)
def _test_lora_api_functionality(
self,
ctx: ServerContext,
case: DiffusionTestCase,
generate_fn: Callable[[str, openai.Client], tuple[str, bytes]],
) -> None:
"""
Test LoRA API functionality with end-to-end validation: merge, unmerge, and set_lora.
This test verifies that each API call succeeds AND that generation works after each operation.
"""
base_url = f"http://localhost:{ctx.port}/v1"
client = self._client(ctx)
# Test 1: unmerge_lora_weights - API should succeed and generation should work
logger.info("[LoRA E2E] Testing unmerge_lora_weights for %s", case.id)
resp = requests.post(
f"{base_url}/unmerge_lora_weights", timeout=_CONTROL_API_TIMEOUT_SECS
)
assert resp.status_code == 200, f"unmerge_lora_weights failed: {resp.text}"
logger.info("[LoRA E2E] Verifying generation after unmerge for %s", case.id)
rid_after_unmerge, _ = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid_after_unmerge is not None, "Generation after unmerge failed"
logger.info("[LoRA E2E] Generation after unmerge succeeded")
# Test 2: merge_lora_weights - API should succeed and generation should work
logger.info("[LoRA E2E] Testing merge_lora_weights for %s", case.id)
resp = requests.post(
f"{base_url}/merge_lora_weights", timeout=_CONTROL_API_TIMEOUT_SECS
)
assert resp.status_code == 200, f"merge_lora_weights failed: {resp.text}"
logger.info("[LoRA E2E] Verifying generation after re-merge for %s", case.id)
rid_after_merge, content_after_merge = (
self._run_generation_with_server_watchdog(ctx, case.id, generate_fn, client)
)
assert rid_after_merge is not None, "Generation after merge failed"
self._validate_lora_consistency(case, content_after_merge, "merge_lora_weights")
logger.info("[LoRA E2E] Generation after merge succeeded")
# Test 3: set_lora (re-set the same adapter) - API should succeed and generation should work
logger.info("[LoRA E2E] Testing set_lora for %s", case.id)
resp = requests.post(
f"{base_url}/set_lora",
json={"lora_nickname": "default"},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert resp.status_code == 200, f"set_lora failed: {resp.text}"
logger.info("[LoRA E2E] Verifying generation after set_lora for %s", case.id)
rid_after_set, content_after_set = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid_after_set is not None, "Generation after set_lora failed"
self._validate_lora_consistency(case, content_after_set, "set_lora")
logger.info("[LoRA E2E] Generation after set_lora succeeded")
# Test 4: list_loras - API should return the expected list of LoRA adapters
logger.info("[LoRA E2E] Testing list_loras for %s", case.id)
resp = requests.get(f"{base_url}/list_loras", timeout=_CONTROL_API_TIMEOUT_SECS)
assert resp.status_code == 200, f"list_loras failed: {resp.text}"
lora_info = resp.json()
logger.info("[LoRA E2E] list_loras returned %s", lora_info)
assert (
isinstance(lora_info["loaded_adapters"], list)
and len(lora_info["loaded_adapters"]) > 0
), "loaded_adapters should be a non-empty list"
assert any(
a.get("nickname") == "default" for a in lora_info["loaded_adapters"]
), f"nickname 'default' not found in loaded_adapters: {lora_info['loaded_adapters']}"
logger.info("[LoRA E2E] list_loras returned expected LoRA adapters")
logger.info("[LoRA E2E] All LoRA API E2E tests passed for %s", case.id)
def _test_lora_dynamic_switch_e2e(
self,
ctx: ServerContext,
case: DiffusionTestCase,
generate_fn: Callable[[str, openai.Client], tuple[str, bytes]],
second_lora_path: str,
) -> None:
"""
Test dynamic LoRA switching with end-to-end validation.
This test verifies that switching between LoRA adapters works correctly
and generation succeeds after each switch.
"""
base_url = f"http://localhost:{ctx.port}/v1"
client = self._client(ctx)
# Test 1: Generate with initial LoRA
logger.info(
"[LoRA Switch E2E] Testing generation with initial LoRA for %s", case.id
)
rid_initial, content_initial = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid_initial is not None, "Generation with initial LoRA failed"
self._validate_lora_consistency(
case, content_initial, "dynamic switch initial LoRA"
)
logger.info("[LoRA Switch E2E] Generation with initial LoRA succeeded")
# Test 2: Switch to second LoRA and generate
logger.info(
"[LoRA Switch E2E] Switching to second LoRA adapter for %s", case.id
)
resp = requests.post(
f"{base_url}/set_lora",
json={"lora_nickname": "lora2", "lora_path": second_lora_path},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert (
resp.status_code == 200
), f"set_lora to second adapter failed: {resp.text}"
logger.info(
"[LoRA Switch E2E] Verifying generation with second LoRA for %s", case.id
)
rid_second, _ = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid_second is not None, "Generation with second LoRA failed"
logger.info("[LoRA Switch E2E] Generation with second LoRA succeeded")
# Test 3: Switch back to original LoRA and generate
logger.info("[LoRA Switch E2E] Switching back to original LoRA for %s", case.id)
resp = requests.post(
f"{base_url}/set_lora",
json={"lora_nickname": "default"},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert resp.status_code == 200, f"set_lora back to default failed: {resp.text}"
logger.info(
"[LoRA Switch E2E] Verifying generation after switching back for %s",
case.id,
)
rid_switched_back, content_switched_back = (
self._run_generation_with_server_watchdog(ctx, case.id, generate_fn, client)
)
assert rid_switched_back is not None, "Generation after switching back failed"
self._validate_lora_consistency(
case, content_switched_back, "dynamic switch default LoRA"
)
logger.info("[LoRA Switch E2E] Generation after switching back succeeded")
logger.info(
"[LoRA Switch E2E] All dynamic switch E2E tests passed for %s", case.id
)
def _test_dynamic_lora_loading(
self,
ctx: ServerContext,
case: DiffusionTestCase,
) -> None:
"""
Test dynamic LoRA loading after server startup.
This test reproduces the LayerwiseOffload + set_lora issue:
- Server starts WITHOUT lora_path (LayerwiseOffloadManager initializes first)
- Then set_lora is called via API to load LoRA dynamically
- This tests the interaction between layerwise offload and dynamic LoRA loading
"""
base_url = f"http://localhost:{ctx.port}/v1"
dynamic_lora_path = case.server_args.dynamic_lora_path
# Call set_lora to load LoRA dynamically after server startup
logger.info(
"[Dynamic LoRA] Loading LoRA dynamically via set_lora API for %s", case.id
)
logger.info("[Dynamic LoRA] LoRA path: %s", dynamic_lora_path)
resp = requests.post(
f"{base_url}/set_lora",
json={"lora_nickname": "default", "lora_path": dynamic_lora_path},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert resp.status_code == 200, f"Dynamic set_lora failed: {resp.text}"
logger.info("[Dynamic LoRA] set_lora succeeded for %s", case.id)
def _test_multi_lora_e2e(
self,
ctx: ServerContext,
case: DiffusionTestCase,
generate_fn: Callable[[str, openai.Client], tuple[str, bytes]],
first_lora_path: str,
second_lora_path: str,
) -> None:
"""
Test multiple LoRA adapters with different set_lora input scenarios.
Tests: basic multi-LoRA, different strengths, cached adapters, switch back to single.
"""
base_url = f"http://localhost:{ctx.port}/v1"
client = self._client(ctx)
# Test 1: Basic multi-LoRA with list format
resp = requests.post(
f"{base_url}/set_lora",
json={
"lora_nickname": ["default", "lora2"],
"lora_path": [first_lora_path, second_lora_path],
"target": "all",
"strength": [0.5, 0.5],
},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert (
resp.status_code == 200
), f"set_lora with multiple adapters failed: {resp.text}"
rid, _ = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid is not None
# Test 2: Different strengths
resp = requests.post(
f"{base_url}/set_lora",
json={
"lora_nickname": ["default", "lora2"],
"lora_path": [first_lora_path, second_lora_path],
"target": "all",
"strength": [0.6, 0.35],
},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert (
resp.status_code == 200
), f"set_lora with different strengths failed: {resp.text}"
rid, _ = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid is not None
# Test 3: Different targets
requests.post(
f"{base_url}/set_lora",
json={"lora_nickname": "default"},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
resp = requests.post(
f"{base_url}/set_lora",
json={
"lora_nickname": ["default", "lora2"],
"lora_path": [first_lora_path, second_lora_path],
"target": ["transformer", "transformer_2"],
"strength": [0.6, 0.35],
},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert (
resp.status_code == 200
), f"set_lora with cached adapters failed: {resp.text}"
rid, _ = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid is not None
# Test 4: Switch back to single LoRA
resp = requests.post(
f"{base_url}/set_lora",
json={"lora_nickname": "default"},
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert (
resp.status_code == 200
), f"set_lora back to single adapter failed: {resp.text}"
rid, content = self._run_generation_with_server_watchdog(
ctx, case.id, generate_fn, client
)
assert rid is not None
self._validate_lora_consistency(case, content, "multi-LoRA default adapter")
logger.info("[Multi-LoRA] All multi-LoRA tests passed for %s", case.id)
def _test_v1_models_endpoint(
self, ctx: ServerContext, case: DiffusionTestCase
) -> None:
"""
Test /v1/models endpoint returns OpenAI-compatible response.
This endpoint is required for sgl-model-gateway router compatibility.
"""
base_url = f"http://localhost:{ctx.port}"
# Test GET /v1/models
logger.info("[Models API] Testing GET /v1/models for %s", case.id)
resp = requests.get(f"{base_url}/v1/models", timeout=_CONTROL_API_TIMEOUT_SECS)
assert resp.status_code == 200, f"/v1/models failed: {resp.text}"
data = resp.json()
assert (
data["object"] == "list"
), f"Expected object='list', got {data.get('object')}"
assert len(data["data"]) >= 1, "Expected at least one model in response"
model = data["data"][0]
assert "id" in model, "Model missing 'id' field"
assert (
model["object"] == "model"
), f"Expected object='model', got {model.get('object')}"
assert (
model["id"] == case.server_args.model_path
), f"Model ID mismatch: expected {case.server_args.model_path}, got {model['id']}"
# Verify extended diffusion-specific fields
assert "num_gpus" in model, "Model missing 'num_gpus' field"
assert "task_type" in model, "Model missing 'task_type' field"
assert "dit_precision" in model, "Model missing 'dit_precision' field"
assert "vae_precision" in model, "Model missing 'vae_precision' field"
assert (
model["num_gpus"] == case.server_args.num_gpus
), f"num_gpus mismatch: expected {case.server_args.num_gpus}, got {model['num_gpus']}"
expected_task_type = get_model_task_type_for_server_args(case.server_args).name
assert model["task_type"] == expected_task_type, (
f"task_type mismatch: expected {expected_task_type}, "
f"got {model['task_type']}"
)
logger.info(
"[Models API] GET /v1/models returned valid response with extended fields"
)
# Test GET /v1/models/{model_path}
model_path = model["id"]
logger.info("[Models API] Testing GET /v1/models/%s", model_path)
resp = requests.get(
f"{base_url}/v1/models/{model_path}", timeout=_CONTROL_API_TIMEOUT_SECS
)
assert resp.status_code == 200, f"/v1/models/{model_path} failed: {resp.text}"
single_model = resp.json()
assert single_model["id"] == model_path, "Single model ID mismatch"
assert single_model["object"] == "model", "Single model object type mismatch"
# Verify extended fields on single model endpoint too
assert "num_gpus" in single_model, "Single model missing 'num_gpus' field"
assert "task_type" in single_model, "Single model missing 'task_type' field"
assert single_model["task_type"] == expected_task_type, (
f"Single model task_type mismatch: expected {expected_task_type}, "
f"got {single_model['task_type']}"
)
logger.info(
"[Models API] GET /v1/models/{model_path} returned valid response with extended fields"
)
# Test GET /v1/models/{non_existent_model} returns 404
logger.info("[Models API] Testing GET /v1/models/non_existent_model")
resp = requests.get(
f"{base_url}/v1/models/non_existent_model",
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert resp.status_code == 404, f"Expected 404, got {resp.status_code}"
error_data = resp.json()
assert "error" in error_data, "404 response missing 'error' field"
assert (
error_data["error"]["code"] == "model_not_found"
), f"Incorrect error code: {error_data['error'].get('code')}"
logger.info("[Models API] GET /v1/models/non_existent returns 404 as expected")
logger.info("[Models API] All /v1/models tests passed for %s", case.id)
def _test_t2v_rejects_input_reference(
self, ctx: ServerContext, case: DiffusionTestCase
) -> None:
if case.server_args.modality != "video":
return
base_url = f"http://localhost:{ctx.port}"
resp = requests.get(f"{base_url}/v1/models", timeout=_CONTROL_API_TIMEOUT_SECS)
assert resp.status_code == 200, f"/v1/models failed: {resp.text}"
data = resp.json().get("data", [])
if not data:
pytest.fail("/v1/models returned empty model list")
task_type = data[0].get("task_type")
if task_type != "T2V":
return
prompt = case.sampling_params.prompt or "test"
payload = {"prompt": prompt, "input_reference": "dummy"}
if case.sampling_params.output_size:
payload["size"] = case.sampling_params.output_size
resp = requests.post(
f"{base_url}/v1/videos",
json=payload,
timeout=_CONTROL_API_TIMEOUT_SECS,
)
assert (
resp.status_code == 400
), f"Expected 400 for T2V input_reference, got {resp.status_code}: {resp.text}"
detail = resp.json().get("detail", "")
assert (
"input_reference is not supported" in detail
), f"Unexpected error detail for T2V input_reference: {detail}"
def test_diffusion_generation(
self,
case: DiffusionTestCase,
diffusion_server: ServerContext,
):
"""Single parametrized test that runs for all cases.
This test performs:
1. Generation
2. Performance validation against baselines
3. Consistency validation against ground truth
Pytest will execute this test once per case in ONE_GPU_CASES,
with test IDs like:
- test_diffusion_generation[qwen_image_text]
- test_diffusion_generation[qwen_image_edit]
- etc.
"""
try:
self._test_diffusion_generation_impl(case, diffusion_server)
except pytest.skip.Exception:
_print_case_log_separator(case.id, "SKIPPED diffusion testcase")
raise
except (KeyboardInterrupt, SystemExit):
raise
except BaseException:
_print_case_log_separator(case.id, "FAILED diffusion testcase")
raise
else:
_print_case_log_separator(case.id, "PASSED diffusion testcase")
def _test_diffusion_generation_impl(
self,
case: DiffusionTestCase,
diffusion_server: ServerContext,
):
# Check if we're in GT generation mode
is_gt_gen_mode = os.environ.get("SGLANG_GEN_GT", "0") == "1"
# GT generation also needs the dynamic set_lora step before generation.
if case.run_lora_dynamic_load_check:
self._test_dynamic_lora_loading(diffusion_server, case)
generate_fn = get_generate_fn(
model_path=case.server_args.model_path,
modality=case.server_args.modality,
sampling_params=case.sampling_params,
)
# Single generation - output is reused for both validations
is_realtime_case = case.sampling_params.realtime_num_chunks is not None
perf_record, content = self.run_and_collect(
diffusion_server,
case.id,
generate_fn,
collect_perf=not is_gt_gen_mode and not is_realtime_case,
)
if is_gt_gen_mode:
# GT generation mode: save output and skip all validations/tests
self._save_gt_output(case, content)
return
failures: list[tuple[str, str]] = []
def run_case_check(name: str, fn: Callable[[], None]) -> None:
try:
fn()
except BaseException as exc:
if isinstance(exc, (KeyboardInterrupt, SystemExit)):
raise
failures.append((name, str(exc)))
if is_realtime_case:
run_case_check(
"performance",
lambda: validate_realtime_perf_stats(
case.id,
pop_realtime_perf_stats(case.id),
case.sampling_params.realtime_perf_thresholds,
ignore_initial_chunks=(
case.sampling_params.realtime_perf_ignore_initial_chunks
),
),
)
else:
run_case_check(
"performance",
lambda: self._validate_and_record(case, perf_record),
)
if case.server_args.custom_validator == "mesh":
from sglang.multimodal_gen.test.server.test_server_utils import (
MESH_OUTPUT_PATHS,
validate_mesh_correctness,
)
def validate_mesh_output() -> None:
mesh_path = MESH_OUTPUT_PATHS.pop(case.id, None)
if mesh_path:
validate_mesh_correctness(mesh_path)
run_case_check("mesh correctness", validate_mesh_output)
if case.run_models_api_check:
run_case_check(
"/v1/models endpoint",
lambda: self._test_v1_models_endpoint(diffusion_server, case),
)
if case.run_t2v_input_reference_check:
run_case_check(
"t2v input_reference rejection",
lambda: self._test_t2v_rejects_input_reference(diffusion_server, case),
)
if case.run_consistency_check:
run_case_check(
"consistency",
lambda: self._validate_consistency(case, content),
)
if case.run_lora_basic_api_check:
run_case_check(
"LoRA basic API",
lambda: self._test_lora_api_functionality(
diffusion_server, case, generate_fn
),
)
if case.run_lora_dynamic_switch_check:
run_case_check(
"LoRA dynamic switch",
lambda: self._test_lora_dynamic_switch_e2e(
diffusion_server,
case,
generate_fn,
case.server_args.second_lora_path,
),
)
if case.run_multi_lora_api_check:
run_case_check(
"multi-LoRA API",
lambda: self._test_multi_lora_e2e(
diffusion_server,
case,
generate_fn,
case.server_args.lora_path,
case.server_args.second_lora_path,
),
)
if failures:
formatted_failures = []
for name, message in failures:
if "\n" in message:
formatted_failures.append(f"[{name}]\n{message}")
else:
formatted_failures.append(f"[{name}] {message}")
pytest.fail(
f"Diffusion testcase '{case.id}' failed {len(failures)} check(s):\n\n"
+ "\n\n".join(formatted_failures),
pytrace=False,
)