[diffusion] CI: add minimax-h3 ref2va audio consistency coverage and guard peak vram (#35511)

This commit is contained in:
Mick
2026-08-20 17:22:05 +08:00
committed by GitHub
parent f1b9a1f42a
commit 17313cf4b2
24 changed files with 1617 additions and 113 deletions
@@ -8,7 +8,7 @@ import os
import tempfile
import time
from contextlib import ExitStack
from dataclasses import dataclass, field
from dataclasses import dataclass, field, replace
from typing import Any, Callable, Iterator, List, Union
import numpy as np
@@ -24,6 +24,7 @@ globally_suppress_loggers()
from sglang.multimodal_gen import envs
from sglang.multimodal_gen.runtime.distributed import (
get_replica_group,
get_sp_group,
get_tp_rank,
get_tp_world_size,
@@ -151,6 +152,12 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
self.pipeline: ComposedPipelineBase = None
self.init_device_and_model()
self._load_peak_reserved_mb = (
0.0
if current_platform.is_cpu()
else capture_memory_snapshot().peak_reserved_mb
)
self._runtime_peak_reserved_mb = 0.0
self.sp_group = get_sp_group()
self.sp_cpu_group = self.sp_group.cpu_group
self.tp_group = get_tp_group()
@@ -464,11 +471,7 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
output_batch = None
forward_failed = False
try:
if (
self.is_output_rank
and not current_platform.is_cpu()
and not current_platform.is_mps()
):
if not current_platform.is_cpu() and not current_platform.is_mps():
torch.get_device_module().reset_peak_memory_stats()
start_time = (
@@ -510,7 +513,6 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
return result
output_batch = self._to_output_batch(result)
self._record_output_peak_memory(output_batch)
output_metrics = self._iter_output_metrics(output_batch)
if self.is_output_rank and output_metrics and not current_platform.is_cpu():
@@ -518,6 +520,19 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
for metrics in output_metrics:
metrics.record_memory_snapshot("after_forward", peak_snapshot)
duration_ms = (time.monotonic() - start_time) * 1000
for metrics in output_metrics:
metrics.total_duration_ms = duration_ms
self._materialize_output_transport(output_batch, req, save_output_paths)
self._record_output_peak_memory(output_batch)
collect_perf = (
req.perf_dump_path is not None or envs.SGLANG_DIFFUSION_STAGE_LOGGING
)
if collect_perf and not req.is_warmup:
self._record_replica_peak_memory(output_metrics)
if (
self.is_output_rank
and not req.suppress_logs
@@ -526,12 +541,6 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
):
self.do_mem_analysis(output_batch)
duration_ms = (time.monotonic() - start_time) * 1000
for metrics in output_metrics:
metrics.total_duration_ms = duration_ms
self._materialize_output_transport(output_batch, req, save_output_paths)
if (
not current_platform.is_cpu()
and output_batch.output is None
@@ -690,9 +699,38 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
return np.asarray(materialized.frames)
def _record_output_peak_memory(self, output_batch: OutputBatch) -> None:
if not self.is_output_rank or current_platform.is_cpu():
if current_platform.is_cpu():
return
output_batch.peak_memory_mb = capture_memory_snapshot().peak_reserved_mb
peak_reserved_mb = capture_memory_snapshot().peak_reserved_mb
self._runtime_peak_reserved_mb = max(
self._runtime_peak_reserved_mb, peak_reserved_mb
)
if self.is_output_rank:
output_batch.peak_memory_mb = peak_reserved_mb
def _record_replica_peak_memory(self, output_metrics: list[Any]) -> None:
"""Record replica-wide loading and runtime allocator peaks."""
if not current_platform.is_cuda():
return
peaks = torch.tensor(
[self._load_peak_reserved_mb, self._runtime_peak_reserved_mb],
dtype=torch.float64,
device=current_platform.get_device(self.local_rank),
)
peaks = get_replica_group().all_reduce(peaks, op=torch.distributed.ReduceOp.MAX)
if not self.is_output_rank:
return
snapshot = capture_memory_snapshot()
load_peak_mb, runtime_peak_mb = peaks.tolist()
for metrics in output_metrics:
metrics.record_memory_snapshot(
"load_peak", replace(snapshot, peak_reserved_mb=load_peak_mb)
)
metrics.record_memory_snapshot(
"runtime_peak", replace(snapshot, peak_reserved_mb=runtime_peak_mb)
)
def _forward_group(self, batch: list[Req]) -> OutputBatch:
assert self.pipeline is not None
@@ -181,7 +181,9 @@ class ComposedPipelineBase(ABC):
if model_subfolder is None:
model_path = maybe_download_model(
self.model_path, force_diffusers_model=True
self.model_path,
force_diffusers_model=True,
revision=self.server_args.revision,
)
else:
model_subfolder = os.path.normpath(model_subfolder)
@@ -196,6 +198,7 @@ class ComposedPipelineBase(ABC):
model_root = maybe_download_model(
self.model_path,
allow_patterns=[f"{model_subfolder}/**"],
revision=self.server_args.revision,
)
model_path = os.path.join(model_root, model_subfolder)
@@ -35,6 +35,7 @@ from sglang.multimodal_gen.configs.models.vaes.minimax_h3_video import (
MiniMaxH3VideoVAEArchConfig,
)
from sglang.multimodal_gen.runtime.distributed.parallel_state import get_world_group
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.constants import (
MINIMAX_H3_SUPPORTED_FPS,
)
@@ -321,7 +322,10 @@ def minimax_h3_encode_reference_audio_rows(
waveform = _audio_resampler(int(source_rate))(waveform)
waveform = waveform.to(device)
with _AudioVAEDeterminismContext():
with (
_AudioVAEDeterminismContext(),
set_forward_context(current_timestep=0, attn_metadata=None),
):
audio_data = model.preprocess(
waveform.unsqueeze(1), MINIMAX_H3_AUDIO_SAMPLE_RATE
)
@@ -34,7 +34,10 @@ _DEFAULT_REALESRGAN_FILENAMES_BY_SCALE = {
4: "RealESRGAN_x4.pth",
8: "RealESRGAN_x8.pth",
}
_LOW_MEMORY_TILED_UPSCALE_FREE_BYTES = 2 * 1024**3
# RRDBNet full-frame 1024x1024 inference can request several GiB of cuDNN
# workspace beyond its output tensor. Keep enough headroom to avoid using an
# allocator OOM as the normal tiled-dispatch mechanism.
_FULL_FRAME_UPSCALE_MIN_FREE_BYTES = 12 * 1024**3
_REALESRGAN_TILE_SIZE = 256
_REALESRGAN_TILE_PAD = 32
@@ -343,7 +346,7 @@ class UpscalerModel:
free_bytes, _ = torch.cuda.mem_get_info(self.device)
output_bytes = h * w * self.scale * self.scale * 3 * 4
required_free_bytes = max(
_LOW_MEMORY_TILED_UPSCALE_FREE_BYTES,
_FULL_FRAME_UPSCALE_MIN_FREE_BYTES,
output_bytes * 4,
)
return free_bytes < required_free_bytes
@@ -837,6 +837,7 @@ def maybe_download_model(
is_lora: bool = False,
allow_patterns: list[str] | None = None,
force_diffusers_model: bool = False,
revision: str | None = None,
skip_overlay_resolution: bool = False,
) -> str:
"""
@@ -848,6 +849,7 @@ def maybe_download_model(
download: Whether to download the model from Hugging Face Hub
is_lora: If True, skip model completeness verification (LoRA models don't have transformer/vae directories)
force_diffusers_model: If True, apply diffusers model check. Otherwise it should be a component model
revision: Specific Hugging Face Hub revision to resolve
Returns:
Local path to the model
"""
@@ -917,6 +919,7 @@ def maybe_download_model(
local_dir=local_dir,
local_files_only=True,
max_workers=8,
revision=revision,
)
if _is_revisionless_snapshot_root(local_path):
# A cache miss, so the download below re-resolves and rewrites the ref.
@@ -1009,6 +1012,7 @@ def maybe_download_model(
allow_patterns=allow_patterns,
local_dir=local_dir,
max_workers=8,
revision=revision,
)
if not force_diffusers_model:
@@ -1027,6 +1031,7 @@ def maybe_download_model(
local_dir=local_dir,
max_workers=8,
force_download=True,
revision=revision,
)
if not _verify_diffusers_model_complete(local_path):
raise ValueError(
@@ -141,7 +141,7 @@ def _run_case(case: DiffusionTestCase) -> dict:
if "per_frame_generation" not in perf.stage_metrics:
perf.stage_metrics["per_frame_generation"] = perf.e2e_ms / sp.num_frames
return {
baseline = {
"stages_ms": {k: round(v, 2) for k, v in perf.stage_metrics.items()},
"denoise_step_ms": {
str(k): round(v, 2) for k, v in perf.all_denoise_steps.items()
@@ -150,6 +150,14 @@ def _run_case(case: DiffusionTestCase) -> dict:
"expected_avg_denoise_ms": round(perf.avg_denoise_ms, 2),
"expected_median_denoise_ms": round(perf.median_denoise_ms, 2),
}
if current_platform.is_cuda():
baseline.update(
{
"load_peak_vram_mb": round(perf.load_peak_vram_mb, 2),
"runtime_peak_vram_mb": round(perf.runtime_peak_vram_mb, 2),
}
)
return baseline
finally:
ctx.cleanup()
@@ -43,10 +43,30 @@ def _write_github_step_summary(content: str):
def _write_results_json(results: list, output_path: str = "diffusion-results.json"):
"""Write performance results to JSON file for CI artifact collection."""
try:
existing = []
if os.path.exists(output_path):
try:
with open(output_path, encoding="utf-8") as f:
loaded = json.load(f)
if isinstance(loaded, list):
existing = loaded
except json.JSONDecodeError:
pass
merged = {
(entry.get("class_name"), entry.get("test_name")): entry
for entry in existing
}
merged.update(
{
(entry.get("class_name"), entry.get("test_name")): entry
for entry in results
}
)
with open(output_path, "w") as f:
json.dump(results, f, indent=2)
json.dump(list(merged.values()), f, indent=2)
print(f"[CONFTEST] Wrote results to {output_path}")
except Exception as e:
except (json.JSONDecodeError, OSError) as e:
print(f"[CONFTEST] Failed to write results JSON: {e}")
@@ -63,15 +83,17 @@ def _generate_diffusion_markdown_report(results: list) -> str:
# Main performance table
markdown = header
markdown += "| Test Suite | Test Name | Modality | E2E (ms) | Avg Denoise (ms) | Median Denoise (ms) |\n"
markdown += "| ---------- | --------- | -------- | -------- | ---------------- | ------------------- |\n"
markdown += "| Test Suite | Test Name | Modality | E2E (ms) | Avg Denoise (ms) | Median Denoise (ms) | Load Peak VRAM (MiB) | Runtime Peak VRAM (MiB) |\n"
markdown += "| ---------- | --------- | -------- | -------- | ---------------- | ------------------- | -------------------- | ----------------------- |\n"
for entry in sorted(results, key=lambda x: (x["class_name"], x["test_name"])):
modality = entry.get("modality", "image")
markdown += (
f"| {entry['class_name']} | {entry['test_name']} | {modality} | "
f"{entry['e2e_ms']:.2f} | {entry['avg_denoise_ms']:.2f} | "
f"{entry['median_denoise_ms']:.2f} |\n"
f"{entry['median_denoise_ms']:.2f} | "
f"{entry.get('load_peak_vram_mb', 0):.0f} | "
f"{entry.get('runtime_peak_vram_mb', 0):.0f} |\n"
)
# Video-specific metrics table (if any video tests)
@@ -111,7 +133,7 @@ def pytest_sessionfinish(session):
# Print to stdout (existing behavior)
print("\n\n" + "=" * 35 + " Performance Summary " + "=" * 35)
print(
f"{'Test Suite':<30} | {'Test Name':<20} | {'E2E (ms)':>12} | {'Avg Denoise (ms)':>18} | {'Median Denoise (ms)':>20}"
f"{'Test Suite':<30} | {'Test Name':<20} | {'E2E (ms)':>12} | {'Avg Denoise (ms)':>18} | {'Median Denoise (ms)':>20} | {'Load Peak (MiB)':>15} | {'Runtime Peak (MiB)':>18}"
)
print(
"-" * 30
@@ -123,15 +145,21 @@ def pytest_sessionfinish(session):
+ "-" * 18
+ "-+-"
+ "-" * 20
+ "-+-"
+ "-" * 15
+ "-+-"
+ "-" * 18
)
for entry in sorted_results:
print(
f"{entry['class_name']:<30} | {entry['test_name']:<20} | {entry['e2e_ms']:>12.2f} | "
f"{entry['avg_denoise_ms']:>18.2f} | {entry['median_denoise_ms']:>20.2f}"
f"{entry['avg_denoise_ms']:>18.2f} | {entry['median_denoise_ms']:>20.2f} | "
f"{entry.get('load_peak_vram_mb', 0):>15.0f} | "
f"{entry.get('runtime_peak_vram_mb', 0):>18.0f}"
)
print("=" * 91)
print("=" * 130)
print("\n\n" + "=" * 36 + " Detailed Reports " + "=" * 37)
for entry in sorted_results:
@@ -279,5 +279,9 @@
"default_mean_abs_diff_threshold_image": 8.0,
"default_ssim_threshold_video": 0.92,
"default_psnr_threshold_video": 24.0,
"default_mean_abs_diff_threshold_video": 10.0
"default_mean_abs_diff_threshold_video": 10.0,
"default_audio_spectral_similarity_threshold": 0.95,
"default_audio_waveform_correlation_threshold": 0.90,
"default_audio_rms_db_diff_threshold": 2.0,
"default_audio_duration_diff_threshold": 0.1
}
@@ -489,6 +489,9 @@ ONE_GPU_CASES: list[DiffusionTestCase] = [
"--pipeline-class-name LingBotWorldCausalDMDPipeline --warmup-mode off"
],
text_encoder_cpu_offload=True,
env_vars={
"PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True",
},
),
REALTIME_MODEL_sampling_params,
run_component_accuracy_check=False,
@@ -652,6 +655,7 @@ MINIMAX_H3_FOUR_GPU_H100_CASES = [
output_size="1344x768",
seconds=5,
output_format="mp4",
expect_audio_output=True,
num_outputs_per_prompt=1,
extras={
"task": "fl2va",
@@ -683,7 +687,7 @@ MINIMAX_H3_FOUR_GPU_H100_CASES = [
run_component_accuracy_check=False,
run_models_api_check=False,
run_t2v_input_reference_check=False,
)
),
]
TWO_GPU_CASES = [
@@ -723,6 +727,7 @@ TWO_GPU_CASES = [
output_size="1344x768",
seconds=4,
output_format="mp4",
expect_audio_output=True,
num_outputs_per_prompt=1,
extras={
"task": "t2va",
@@ -744,6 +749,73 @@ TWO_GPU_CASES = [
run_models_api_check=False,
run_t2v_input_reference_check=False,
),
DiffusionTestCase(
"minimax_h3_ref2va_video_audio_2gpu_h100",
DiffusionServerArgs(
model_path="MiniMaxAI/MiniMax-H3",
modality="video",
tp_size=2,
ulysses_degree=1,
extras=[
"--model-variant",
"ref2va",
"--revision",
"42ed227ee7df40d41602854ae760620d6eb651fe",
"--performance-mode",
"memory",
"--layerwise-offload-components",
"dit,text_encoder",
"--component-residency",
"vae=resident",
"--dit-offload-prefetch-size",
"1",
"--dit-layerwise-resident-layers",
"20",
"--enable-torch-compile",
"false",
],
),
DiffusionSamplingParams(
prompt=(
"Follow the motion and appearance of <Video 1> while moving the "
"scene to a quiet moonlit room, and use <Audio 1> as the sound "
"reference with coherent timing."
),
output_size="1344x768",
seconds=4,
output_format="mp4",
expect_audio_output=True,
num_outputs_per_prompt=1,
extras={
"task": "ref2va",
"conditions": [
{
"type": "video_audio",
"uri": (
"https://huggingface.co/MiniMaxAI/MiniMax-H3/resolve/"
"42ed227ee7df40d41602854ae760620d6eb651fe/assets/"
"ref2va.mp4"
),
"role": "reference",
}
],
"target": {
"short_edge": 768,
"aspect_ratio": "16:9",
"duration_seconds": 4.0,
},
"num_inference_steps": 8,
"flow_shift": 12.0,
"audio_flow_shift": 3.0,
"seed": 42,
},
),
run_perf_check=False,
run_consistency_check=True,
run_component_accuracy_check=False,
run_models_api_check=False,
run_t2v_input_reference_check=False,
),
DiffusionTestCase(
"flux2_modelopt_fp8_tp2_t2i",
DiffusionServerArgs(
@@ -11,14 +11,18 @@
"denoise_stage": 0.1,
"non_denoise_stage": 0.5,
"denoise_step": 0.25,
"denoise_agg": 0.15
"denoise_agg": 0.15,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02
},
"pr_test": {
"e2e": 0.25,
"denoise_stage": 0.25,
"non_denoise_stage": 0.8,
"denoise_step": 0.3,
"denoise_agg": 0.2
"denoise_agg": 0.2,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02
}
},
"improvement_reporting": {
@@ -56,6 +60,8 @@
"expected_e2e_ms": 18255.39,
"expected_avg_denoise_ms": 338.78,
"expected_median_denoise_ms": 345.95,
"load_peak_vram_mb": 1126.0,
"runtime_peak_vram_mb": 13180.0,
"estimated_full_test_time_s": 94.0
},
"wan2_1_t2v_1.3b": {
@@ -78,6 +84,8 @@
"expected_e2e_ms": 23418.73,
"expected_avg_denoise_ms": 426.59,
"expected_median_denoise_ms": 427.94,
"load_peak_vram_mb": 4934.0,
"runtime_peak_vram_mb": 18246.0,
"estimated_full_test_time_s": 160.9
},
"zimage_image_t2i": {
@@ -100,6 +108,8 @@
"expected_e2e_ms": 2492.09,
"expected_avg_denoise_ms": 241.53,
"expected_median_denoise_ms": 265.1,
"load_peak_vram_mb": 13120.0,
"runtime_peak_vram_mb": 17776.0,
"estimated_full_test_time_s": 329.8
},
"lingbot_video_moe_t2v": {
@@ -11,14 +11,18 @@
"denoise_stage": 0.1,
"non_denoise_stage": 0.5,
"denoise_step": 0.25,
"denoise_agg": 0.15
"denoise_agg": 0.15,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02
},
"pr_test": {
"e2e": 0.25,
"denoise_stage": 0.25,
"non_denoise_stage": 0.8,
"denoise_step": 0.3,
"denoise_agg": 0.2
"denoise_agg": 0.2,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02
}
},
"improvement_reporting": {
@@ -11,14 +11,18 @@
"denoise_stage": 0.1,
"non_denoise_stage": 0.5,
"denoise_step": 0.25,
"denoise_agg": 0.15
"denoise_agg": 0.15,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02
},
"pr_test": {
"e2e": 0.25,
"denoise_stage": 0.25,
"non_denoise_stage": 0.8,
"denoise_step": 0.3,
"denoise_agg": 0.2
"denoise_agg": 0.2,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02
}
},
"improvement_reporting": {
@@ -99,6 +103,8 @@
"expected_e2e_ms": 12770.15,
"expected_avg_denoise_ms": 247.95,
"expected_median_denoise_ms": 249.01,
"load_peak_vram_mb": 41840.0,
"runtime_peak_vram_mb": 46938.0,
"estimated_full_test_time_s": 133.1
},
"qwen_image_t2i_2_gpus": {
@@ -165,6 +171,8 @@
"expected_e2e_ms": 10192.48,
"expected_avg_denoise_ms": 192.9,
"expected_median_denoise_ms": 190.02,
"load_peak_vram_mb": 41840.0,
"runtime_peak_vram_mb": 46938.0,
"estimated_full_test_time_s": 133.2
},
"ideogram4_fp8_t2i": {
@@ -294,6 +302,8 @@
"expected_e2e_ms": 6693.01,
"expected_avg_denoise_ms": 128.85,
"expected_median_denoise_ms": 130.22,
"load_peak_vram_mb": 23594.0,
"runtime_peak_vram_mb": 28540.0,
"estimated_full_test_time_s": 127.4
},
"flux_2_image_t2i": {
@@ -361,6 +371,8 @@
"expected_e2e_ms": 23139.56,
"expected_avg_denoise_ms": 436.76,
"expected_median_denoise_ms": 445.86,
"load_peak_vram_mb": 64344.0,
"runtime_peak_vram_mb": 70560.0,
"estimated_full_test_time_s": 145.2
},
"flux_2_klein_image_t2i": {
@@ -382,6 +394,8 @@
"expected_e2e_ms": 399.62,
"expected_avg_denoise_ms": 30.0,
"expected_median_denoise_ms": 24.14,
"load_peak_vram_mb": 8504.0,
"runtime_peak_vram_mb": 13732.0,
"estimated_full_test_time_s": 120.5
},
"flux_2_klein_base_image_t2i": {
@@ -449,6 +463,8 @@
"expected_e2e_ms": 6112.68,
"expected_avg_denoise_ms": 116.49,
"expected_median_denoise_ms": 117.28,
"load_peak_vram_mb": 8504.0,
"runtime_peak_vram_mb": 13754.0,
"estimated_full_test_time_s": 124.4
},
"flux_2_ti2i": {
@@ -516,6 +532,8 @@
"expected_e2e_ms": 45445.95,
"expected_avg_denoise_ms": 871.81,
"expected_median_denoise_ms": 889.93,
"load_peak_vram_mb": 64344.0,
"runtime_peak_vram_mb": 70564.0,
"estimated_full_test_time_s": 168.9
},
"flux_2_ti2i_multi_image_cache_dit": {
@@ -583,6 +601,8 @@
"expected_e2e_ms": 24192.8,
"expected_avg_denoise_ms": 461.7,
"expected_median_denoise_ms": 53.69,
"load_peak_vram_mb": 64344.0,
"runtime_peak_vram_mb": 71904.0,
"estimated_full_test_time_s": 148.6
},
"flux_image_t2i_2_gpus": {
@@ -649,6 +669,8 @@
"expected_e2e_ms": 4244.27,
"expected_avg_denoise_ms": 79.98,
"expected_median_denoise_ms": 81.01,
"load_peak_vram_mb": 23284.0,
"runtime_peak_vram_mb": 28148.0,
"estimated_full_test_time_s": 125.5
},
"zimage_image_t2i": {
@@ -674,6 +696,8 @@
"expected_e2e_ms": 906.14,
"expected_avg_denoise_ms": 82.61,
"expected_median_denoise_ms": 90.0,
"load_peak_vram_mb": 13120.0,
"runtime_peak_vram_mb": 17776.0,
"estimated_full_test_time_s": 116.3
},
"zimage_image_t2i_fp8": {
@@ -699,6 +723,8 @@
"expected_e2e_ms": 1131.68,
"expected_avg_denoise_ms": 103.24,
"expected_median_denoise_ms": 108.73,
"load_peak_vram_mb": 11344.0,
"runtime_peak_vram_mb": 15868.0,
"estimated_full_test_time_s": 123.7
},
"zimage_image_t2i_multi_lora": {
@@ -724,6 +750,8 @@
"expected_e2e_ms": 910.85,
"expected_avg_denoise_ms": 83.08,
"expected_median_denoise_ms": 89.93,
"load_peak_vram_mb": 14196.0,
"runtime_peak_vram_mb": 17612.0,
"estimated_full_test_time_s": 162.1
},
"zimage_image_t2i_2_gpus": {
@@ -749,6 +777,8 @@
"expected_e2e_ms": 752.84,
"expected_avg_denoise_ms": 51.13,
"expected_median_denoise_ms": 54.3,
"load_peak_vram_mb": 13120.0,
"runtime_peak_vram_mb": 17602.0,
"estimated_full_test_time_s": 75.0
},
"qwen_image_edit_ti2i": {
@@ -816,6 +846,8 @@
"expected_e2e_ms": 32832.9,
"expected_avg_denoise_ms": 633.05,
"expected_median_denoise_ms": 636.16,
"load_peak_vram_mb": 43244.0,
"runtime_peak_vram_mb": 48250.0,
"estimated_full_test_time_s": 153.6
},
"joyai_image_edit_ti2i": {
@@ -873,6 +905,8 @@
"expected_e2e_ms": 27693.61,
"expected_avg_denoise_ms": 666.89,
"expected_median_denoise_ms": 673.29,
"load_peak_vram_mb": 35324.0,
"runtime_peak_vram_mb": 45124.0,
"estimated_full_test_time_s": 117.6
},
"qwen_image_t2i_cache_dit_enabled": {
@@ -939,6 +973,8 @@
"expected_e2e_ms": 4531.7,
"expected_avg_denoise_ms": 84.06,
"expected_median_denoise_ms": 53.48,
"load_peak_vram_mb": 41840.0,
"runtime_peak_vram_mb": 47034.0,
"estimated_full_test_time_s": 124.9
},
"wan2_1_t2v_1.3b_teacache_enabled": {
@@ -1005,6 +1041,8 @@
"expected_e2e_ms": 4739.06,
"expected_avg_denoise_ms": 76.99,
"expected_median_denoise_ms": 98.28,
"load_peak_vram_mb": 7814.0,
"runtime_peak_vram_mb": 18078.0,
"estimated_full_test_time_s": 126.0
},
"wan2_1_t2v_1.3b": {
@@ -1071,6 +1109,8 @@
"expected_e2e_ms": 8106.87,
"expected_avg_denoise_ms": 143.82,
"expected_median_denoise_ms": 144.57,
"load_peak_vram_mb": 7814.0,
"runtime_peak_vram_mb": 18078.0,
"estimated_full_test_time_s": 129.3
},
"wan2_1_t2v_1.3b_cfg_parallel": {
@@ -1137,6 +1177,8 @@
"expected_e2e_ms": 4435.63,
"expected_avg_denoise_ms": 73.51,
"expected_median_denoise_ms": 74.05,
"load_peak_vram_mb": 5422.0,
"runtime_peak_vram_mb": 11972.0,
"estimated_full_test_time_s": 127.6
},
"turbo_wan2_1_t2v_1.3b": {
@@ -1158,6 +1200,8 @@
"expected_e2e_ms": 959.14,
"expected_avg_denoise_ms": 73.38,
"expected_median_denoise_ms": 72.78,
"load_peak_vram_mb": 26256.0,
"runtime_peak_vram_mb": 34340.0,
"estimated_full_test_time_s": 124.7
},
"ltx_2_two_stage_t2v": {
@@ -1225,6 +1269,8 @@
"expected_e2e_ms": 6313.7,
"expected_avg_denoise_ms": 118.58,
"expected_median_denoise_ms": 109.14,
"load_peak_vram_mb": 43920.0,
"runtime_peak_vram_mb": 57528.0,
"estimated_full_test_time_s": 345.4
},
"wan2_2_ti2v_5b": {
@@ -1291,6 +1337,8 @@
"expected_e2e_ms": 14853.56,
"expected_avg_denoise_ms": 253.29,
"expected_median_denoise_ms": 256.21,
"load_peak_vram_mb": 17610.0,
"runtime_peak_vram_mb": 39856.0,
"estimated_full_test_time_s": 141.7
},
"qwen_image_edit_2509_ti2i": {
@@ -1348,6 +1396,8 @@
"expected_e2e_ms": 38630.0,
"expected_avg_denoise_ms": 938.38,
"expected_median_denoise_ms": 945.14,
"load_peak_vram_mb": 43244.0,
"runtime_peak_vram_mb": 47874.0,
"estimated_full_test_time_s": 160.2
},
"qwen_image_layered_i2i": {
@@ -1412,6 +1462,8 @@
"expected_e2e_ms": 33451.98,
"expected_avg_denoise_ms": 661.05,
"expected_median_denoise_ms": 664.26,
"load_peak_vram_mb": 58204.0,
"runtime_peak_vram_mb": 61846.0,
"estimated_full_test_time_s": 161.5
},
"fastwan2_2_ti2v_5b": {
@@ -1431,6 +1483,8 @@
"expected_e2e_ms": 2444.17,
"expected_avg_denoise_ms": 83.86,
"expected_median_denoise_ms": 112.67,
"load_peak_vram_mb": 17610.0,
"runtime_peak_vram_mb": 39054.0,
"estimated_full_test_time_s": 125.2
},
"fast_hunyuan_video": {
@@ -1454,6 +1508,8 @@
"expected_e2e_ms": 9123.68,
"expected_avg_denoise_ms": 871.39,
"expected_median_denoise_ms": 1010.9,
"load_peak_vram_mb": 27320.0,
"runtime_peak_vram_mb": 35926.0,
"estimated_full_test_time_s": 77.0
},
"wan2_2_i2v_a14b_2gpu": {
@@ -1511,6 +1567,8 @@
"expected_e2e_ms": 73811.61,
"expected_avg_denoise_ms": 1689.73,
"expected_median_denoise_ms": 1714.58,
"load_peak_vram_mb": 6440.0,
"runtime_peak_vram_mb": 26322.0,
"estimated_full_test_time_s": 264.1
},
"wan2_1_i2v_14b_480P_2gpu": {
@@ -1579,6 +1637,8 @@
"expected_e2e_ms": 34055.55,
"expected_avg_denoise_ms": 653.89,
"expected_median_denoise_ms": 656.68,
"load_peak_vram_mb": 35350.0,
"runtime_peak_vram_mb": 42674.0,
"estimated_full_test_time_s": 243.0
},
"wan2_1_i2v_14b_720P_2gpu": {
@@ -1647,6 +1707,8 @@
"expected_e2e_ms": 98831.21,
"expected_avg_denoise_ms": 1923.51,
"expected_median_denoise_ms": 1933.7,
"load_peak_vram_mb": 35350.0,
"runtime_peak_vram_mb": 51230.0,
"estimated_full_test_time_s": 248.9
},
"wan2_2_t2v_a14b_2gpu": {
@@ -1703,6 +1765,8 @@
"expected_e2e_ms": 83297.78,
"expected_avg_denoise_ms": 2029.88,
"expected_median_denoise_ms": 2020.71,
"load_peak_vram_mb": 6440.0,
"runtime_peak_vram_mb": 21524.0,
"estimated_full_test_time_s": 204.3
},
"wan2_1_t2v_14b_2gpu": {
@@ -1769,6 +1833,8 @@
"expected_e2e_ms": 24072.24,
"expected_avg_denoise_ms": 461.16,
"expected_median_denoise_ms": 467.69,
"load_peak_vram_mb": 31246.0,
"runtime_peak_vram_mb": 37788.0,
"estimated_full_test_time_s": 173.2
},
"wan2_2_t2v_a14b_lora_2gpu": {
@@ -1825,6 +1891,8 @@
"expected_e2e_ms": 62134.19,
"expected_avg_denoise_ms": 1437.82,
"expected_median_denoise_ms": 1416.85,
"load_peak_vram_mb": 6784.0,
"runtime_peak_vram_mb": 21450.0,
"estimated_full_test_time_s": 181.8
},
"wan2_1_t2v_1_3b_lora_1gpu": {
@@ -1891,6 +1959,8 @@
"expected_e2e_ms": 8138.0,
"expected_avg_denoise_ms": 146.48,
"expected_median_denoise_ms": 147.68,
"load_peak_vram_mb": 7814.0,
"runtime_peak_vram_mb": 18350.0,
"estimated_full_test_time_s": 129.6
},
"wan2_1_i2v_14b_lora_2gpu": {
@@ -1959,6 +2029,8 @@
"expected_e2e_ms": 98305.51,
"expected_avg_denoise_ms": 1913.15,
"expected_median_denoise_ms": 1922.21,
"load_peak_vram_mb": 36266.0,
"runtime_peak_vram_mb": 51132.0,
"estimated_full_test_time_s": 248.2
},
"flux_2_image_t2i_2_gpus": {
@@ -2026,6 +2098,8 @@
"expected_e2e_ms": 13216.13,
"expected_avg_denoise_ms": 242.72,
"expected_median_denoise_ms": 246.92,
"load_peak_vram_mb": 33110.0,
"runtime_peak_vram_mb": 38262.0,
"estimated_full_test_time_s": 135.3
},
"qwen_image_edit_2511_ti2i": {
@@ -2083,6 +2157,8 @@
"expected_e2e_ms": 22989.11,
"expected_avg_denoise_ms": 551.91,
"expected_median_denoise_ms": 555.79,
"load_peak_vram_mb": 43244.0,
"runtime_peak_vram_mb": 47874.0,
"estimated_full_test_time_s": 143.7
},
"fsdp-inference": {
@@ -2108,6 +2184,8 @@
"expected_e2e_ms": 1042.43,
"expected_avg_denoise_ms": 82.26,
"expected_median_denoise_ms": 81.75,
"load_peak_vram_mb": 7176.0,
"runtime_peak_vram_mb": 12634.0,
"estimated_full_test_time_s": 122.7
},
"hunyuan3d_shape_gen": {
@@ -2175,6 +2253,8 @@
"expected_e2e_ms": 237254.68,
"expected_avg_denoise_ms": 32.68,
"expected_median_denoise_ms": 30.2,
"load_peak_vram_mb": 876.0,
"runtime_peak_vram_mb": 6094.0,
"estimated_full_test_time_s": 420.1
},
"wan2_1_t2v_1.3b_frame_interp_2x": {
@@ -2241,6 +2321,8 @@
"expected_e2e_ms": 8273.83,
"expected_avg_denoise_ms": 146.61,
"expected_median_denoise_ms": 147.3,
"load_peak_vram_mb": 7814.0,
"runtime_peak_vram_mb": 18080.0,
"estimated_full_test_time_s": 129.3
},
"flux_2_image_t2i_upscaling_4x": {
@@ -2308,6 +2390,8 @@
"expected_e2e_ms": 22732.26,
"expected_avg_denoise_ms": 429.1,
"expected_median_denoise_ms": 438.64,
"load_peak_vram_mb": 64344.0,
"runtime_peak_vram_mb": 70620.0,
"estimated_full_test_time_s": 145.1
},
"wan2_1_t2v_1.3b_upscaling_4x": {
@@ -2374,6 +2458,8 @@
"expected_e2e_ms": 8333.66,
"expected_avg_denoise_ms": 144.68,
"expected_median_denoise_ms": 144.74,
"load_peak_vram_mb": 7814.0,
"runtime_peak_vram_mb": 24380.0,
"estimated_full_test_time_s": 129.3
},
"wan2_1_t2v_1.3b_frame_interp_2x_upscaling_4x": {
@@ -2440,6 +2526,8 @@
"expected_e2e_ms": 8099.03,
"expected_avg_denoise_ms": 143.71,
"expected_median_denoise_ms": 144.67,
"load_peak_vram_mb": 7814.0,
"runtime_peak_vram_mb": 24388.0,
"estimated_full_test_time_s": 129.4
},
"ltx_2.3_one_stage_ti2v": {
@@ -2489,6 +2577,8 @@
"expected_e2e_ms": 16722.42,
"expected_avg_denoise_ms": 520.0,
"expected_median_denoise_ms": 494.9,
"load_peak_vram_mb": 46278.0,
"runtime_peak_vram_mb": 49390.0,
"estimated_full_test_time_s": 144.2
},
"ltx_2.3_two_stage_t2v_2gpus": {
@@ -2546,6 +2636,8 @@
"expected_e2e_ms": 6349.58,
"expected_avg_denoise_ms": 158.2,
"expected_median_denoise_ms": 145.66,
"load_peak_vram_mb": 47234.0,
"runtime_peak_vram_mb": 59544.0,
"estimated_full_test_time_s": 160.0
},
"ltx_2_3_two_stage_ti2v_2gpus": {
@@ -2603,6 +2695,8 @@
"expected_e2e_ms": 9687.04,
"expected_avg_denoise_ms": 250.16,
"expected_median_denoise_ms": 242.79,
"load_peak_vram_mb": 47234.0,
"runtime_peak_vram_mb": 60286.0,
"estimated_full_test_time_s": 170.0
},
"longlive2_t2v": {
@@ -2619,6 +2713,8 @@
"expected_e2e_ms": 5648.49,
"expected_avg_denoise_ms": 477.49,
"expected_median_denoise_ms": 56.74,
"load_peak_vram_mb": 16110.0,
"runtime_peak_vram_mb": 45756.0,
"estimated_full_test_time_s": 153.1
},
"longlive2_i2v": {
@@ -2635,6 +2731,8 @@
"expected_e2e_ms": 9086.81,
"expected_avg_denoise_ms": 492.02,
"expected_median_denoise_ms": 151.9,
"load_peak_vram_mb": 16110.0,
"runtime_peak_vram_mb": 45756.0,
"estimated_full_test_time_s": 149.4
},
"lingbot_video_moe_t2v": {
@@ -2651,6 +2749,8 @@
"expected_e2e_ms": 0.0,
"expected_avg_denoise_ms": 0.0,
"expected_median_denoise_ms": 0.0,
"load_peak_vram_mb": 42814.0,
"runtime_peak_vram_mb": 73256.0,
"estimated_full_test_time_s": 126.0
},
"ltx_2_3_hq_pipeline": {
@@ -2695,6 +2795,8 @@
"expected_e2e_ms": 15974.21,
"expected_avg_denoise_ms": 802.06,
"expected_median_denoise_ms": 747.62,
"load_peak_vram_mb": 56048.0,
"runtime_peak_vram_mb": 59804.0,
"estimated_full_test_time_s": 363.2
},
"qwen_image_t2i_cache_dit_scm_config_diffusers_1gpu": {
@@ -2801,6 +2903,14 @@
"expected_median_denoise_ms": 0.0,
"estimated_full_test_time_s": 55.4
},
"minimax_h3_ref2va_video_audio_2gpu_h100": {
"stages_ms": {},
"denoise_step_ms": {},
"expected_e2e_ms": 0.0,
"expected_avg_denoise_ms": 0.0,
"expected_median_denoise_ms": 0.0,
"estimated_full_test_time_s": 266.7
},
"minimax_h3_t2va_2gpu_h100": {
"stages_ms": {
"InputValidationStage": 0.05,
@@ -2826,6 +2936,8 @@
"expected_e2e_ms": 19700.0,
"expected_avg_denoise_ms": 2045.39,
"expected_median_denoise_ms": 2332.99,
"load_peak_vram_mb": 45428.0,
"runtime_peak_vram_mb": 63312.0,
"estimated_full_test_time_s": 208.0
},
"mova_360p_tp2": {
@@ -26,6 +26,7 @@ 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 (
RealtimeChunkStats,
pop_realtime_key_frames,
pop_realtime_perf_stats,
validate_realtime_perf_stats,
@@ -51,10 +52,17 @@ from sglang.multimodal_gen.test.test_utils import (
_consistency_gt_filenames,
_get_consistency_gt_dir,
action_gt_exists,
audio_gt_exists,
compare_audio_with_gt,
compare_with_gt,
encode_audio_gt_wav,
extract_audio_pcm_from_video_bytes,
extract_key_frames_from_video,
get_action_consistency_gt_candidates,
get_action_consistency_gt_remote_files,
get_audio_consistency_gt_candidates,
get_audio_consistency_gt_remote_files,
get_audio_consistency_thresholds,
get_consistency_gt_candidates,
get_consistency_gt_remote_files,
get_consistency_threshold_path,
@@ -63,7 +71,9 @@ from sglang.multimodal_gen.test.test_utils import (
gt_exists,
image_bytes_to_numpy,
load_action_consistency_gt,
load_audio_consistency_gt,
load_consistency_gt,
save_audio_gt_artifact,
save_consistency_failure_artifact,
save_missing_consistency_gt_artifact,
wait_for_req_perf_record,
@@ -403,18 +413,7 @@ class DiffusionServerBase:
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": {},
},
)()
scenario = ScenarioConfig({}, {}, 0, 0, 0)
if not is_baseline_generation_mode:
missing_scenario = True
@@ -450,6 +449,29 @@ class DiffusionServerBase:
)
return
if current_platform.is_cuda():
expected_load_peak_vram_mb = scenario.load_peak_vram_mb
expected_runtime_peak_vram_mb = scenario.runtime_peak_vram_mb
if (
expected_load_peak_vram_mb is None
or expected_runtime_peak_vram_mb is None
):
self._dump_baseline_for_testcase(case, summary, missing_scenario)
pytest.fail(
f"Testcase '{case.id}' is missing a load/runtime peak VRAM "
f"baseline in {get_perf_baseline_path()}"
)
try:
validator.validate_peak_vram(
summary,
expected_load_peak_vram_mb,
expected_runtime_peak_vram_mb,
)
except AssertionError as e:
logger.error(f"Peak VRAM validation failed for {case.id}:\n{e}")
self._dump_baseline_for_testcase(case, summary, missing_scenario)
raise
# only run performance validation if run_perf_check is True
try:
validator.validate(perf_record, case.sampling_params.num_frames)
@@ -458,12 +480,93 @@ class DiffusionServerBase:
self._dump_baseline_for_testcase(case, summary, missing_scenario)
raise
self._record_performance_result(case, summary)
def _validate_realtime_performance(
self,
ctx: ServerContext,
case: DiffusionTestCase,
chunk_stats: list[RealtimeChunkStats],
) -> None:
validate_realtime_perf_stats(
case.id,
chunk_stats,
case.sampling_params.realtime_perf_thresholds,
ignore_initial_chunks=(
case.sampling_params.realtime_perf_ignore_initial_chunks
),
)
if not case.run_perf_check or not current_platform.is_cuda():
return
request_id = next(
(stat.request_id for stat in reversed(chunk_stats) if stat.request_id),
None,
)
if request_id is None:
pytest.fail(f"{case.id}: realtime chunk stats are missing request IDs")
perf_record = wait_for_req_perf_record(
request_id,
ctx.perf_log_path,
timeout=30,
)
if perf_record is None:
pytest.fail(f"{case.id}: realtime request performance record is missing")
scenario = BASELINE_CONFIG.scenarios.get(case.id)
if scenario is None:
pytest.fail(f"Testcase '{case.id}' not found in {get_perf_baseline_path()}")
validator = PerformanceValidator(
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)
self._record_performance_result(case, summary)
if os.environ.get("SGLANG_GEN_BASELINE", "0") == "1":
logger.info(
"%s realtime peak VRAM baseline: load=%.0fMiB, runtime=%.0fMiB",
case.id,
summary.load_peak_vram_mb,
summary.runtime_peak_vram_mb,
)
return
if scenario.load_peak_vram_mb is None or scenario.runtime_peak_vram_mb is None:
pytest.fail(
f"Testcase '{case.id}' is missing a load/runtime peak VRAM "
f"baseline in {get_perf_baseline_path()}; measured "
f"load={summary.load_peak_vram_mb:.0f}MiB, "
f"runtime={summary.runtime_peak_vram_mb:.0f}MiB"
)
try:
validator.validate_peak_vram(
summary,
scenario.load_peak_vram_mb,
scenario.runtime_peak_vram_mb,
)
except AssertionError as e:
logger.error(f"Peak VRAM validation failed for {case.id}:\n{e}")
raise
def _record_performance_result(
self,
case: DiffusionTestCase,
summary: PerformanceSummary,
) -> None:
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,
"load_peak_vram_mb": summary.load_peak_vram_mb,
"runtime_peak_vram_mb": summary.runtime_peak_vram_mb,
"stage_metrics": summary.stage_metrics,
"sampled_steps": summary.sampled_steps,
}
@@ -495,7 +598,9 @@ class DiffusionServerBase:
(
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"
f"median_denoise={summary.median_denoise_ms:.2f}ms, "
f"load_peak_vram={summary.load_peak_vram_mb:.0f}MiB, "
f"runtime_peak_vram={summary.runtime_peak_vram_mb:.0f}MiB"
),
]
if scenario is not None:
@@ -505,6 +610,16 @@ class DiffusionServerBase:
f"avg_denoise={scenario.expected_avg_denoise_ms:.2f}ms, "
f"median_denoise={scenario.expected_median_denoise_ms:.2f}ms"
)
if (
scenario is not None
and scenario.load_peak_vram_mb is not None
and scenario.runtime_peak_vram_mb is not None
):
lines.append(
" peak_vram_baseline: "
f"load={scenario.load_peak_vram_mb:.0f}MiB, "
f"runtime={scenario.runtime_peak_vram_mb:.0f}MiB"
)
if summary.stage_metrics:
stages = ", ".join(
f"{name}={duration:.2f}ms"
@@ -544,6 +659,14 @@ class DiffusionServerBase:
"expected_median_denoise_ms": round(summary.median_denoise_ms, 2),
}
if current_platform.is_cuda():
baseline.update(
{
"load_peak_vram_mb": round(summary.load_peak_vram_mb, 2),
"runtime_peak_vram_mb": round(summary.runtime_peak_vram_mb, 2),
}
)
if measured_full_time is not None:
baseline["estimated_full_test_time_s"] = round(measured_full_time, 1)
@@ -612,6 +735,15 @@ class DiffusionServerBase:
logger.info(
"[Artifact] Saved missing consistency GT: %s", artifact_path
)
if case.sampling_params.expect_audio_output:
audio_path = save_audio_gt_artifact(
os.environ.get("SGLANG_DIFFUSION_ARTIFACT_DIR"),
case.id,
num_gpus,
extract_audio_pcm_from_video_bytes(content),
)
if audio_path is not None:
logger.info("[Artifact] Saved missing audio GT: %s", audio_path)
if _get_consistency_gt_dir() is not None:
names = ", ".join(
get_consistency_gt_candidates(
@@ -739,6 +871,67 @@ Pinned revision used by this check: {SGL_TEST_FILES_CI_DATA_REVISION}
f"max_mean_abs_diff={result.max_mean_abs_diff:.4f})"
)
if case.sampling_params.expect_audio_output:
self._validate_audio_consistency(case, content)
def _validate_audio_consistency(
self,
case: DiffusionTestCase,
content: bytes,
) -> None:
num_gpus = case.server_args.num_gpus
output_audio = extract_audio_pcm_from_video_bytes(content)
if not audio_gt_exists(case.id, num_gpus):
artifact_path = save_audio_gt_artifact(
os.environ.get("SGLANG_DIFFUSION_ARTIFACT_DIR"),
case.id,
num_gpus,
output_audio,
)
if artifact_path is not None:
logger.info("[Artifact] Saved missing audio GT: %s", artifact_path)
names = ", ".join(get_audio_consistency_gt_candidates(case.id, num_gpus))
pytest.fail(f"Audio GT not found for {case.id}. Expected one of: {names}")
gt_audio = load_audio_consistency_gt(case.id, num_gpus)
result = compare_audio_with_gt(
output_audio,
gt_audio,
get_audio_consistency_thresholds(case.id),
)
if not result.passed:
gt_remote_info = "\n".join(
f" - {filename}: {url}"
for filename, url in get_audio_consistency_gt_remote_files(
case.id, num_gpus
)
)
pytest.fail(
f"Audio consistency check failed for {case.id}:\n"
f" Metrics: spectral_similarity={result.spectral_similarity:.4f}, "
f"waveform_correlation={result.waveform_correlation:.4f}, "
f"rms_db_diff={result.rms_db_diff:.4f}, "
f"duration_diff={result.duration_diff:.4f}s\n"
f" Thresholds: spectral_similarity>="
f"{result.thresholds.spectral_similarity_threshold}, "
f"waveform_correlation>="
f"{result.thresholds.waveform_correlation_threshold}, "
f"rms_db_diff<={result.thresholds.rms_db_diff_threshold}, "
f"duration_diff<={result.thresholds.duration_diff_threshold}s\n"
f" Compared GT files and links:\n{gt_remote_info}"
)
logger.info(
"[Consistency] %s: PASSED audio GT check "
"(spectral_similarity=%.4f, waveform_correlation=%.4f, "
"rms_db_diff=%.4f, duration_diff=%.4fs)",
case.id,
result.spectral_similarity,
result.waveform_correlation,
result.rms_db_diff,
result.duration_diff,
)
def _extract_action_array(
self,
payload: dict[str, Any],
@@ -865,16 +1058,22 @@ Pinned revision used by this check: {SGL_TEST_FILES_CI_DATA_REVISION}
logger.warning(
f"{case.id}: expected 3 frames, got {len(frames)}, skipping frame save"
)
return
else:
# Save frames (reuse naming from _consistency_gt_filenames)
filenames = _consistency_gt_filenames(case.id, num_gpus, is_video=True)
from PIL import Image
# 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}")
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}")
if case.sampling_params.expect_audio_output:
audio_path = out_dir / f"{case.id}_{num_gpus}gpu_audio.wav"
audio_path.write_bytes(
encode_audio_gt_wav(extract_audio_pcm_from_video_bytes(content))
)
logger.info("Saved GT audio: %s", audio_path)
else:
# Save image
from sglang.multimodal_gen.test.test_utils import detect_image_format
@@ -1372,15 +1571,13 @@ Pinned revision used by this check: {SGL_TEST_FILES_CI_DATA_REVISION}
failures.append((name, str(exc)))
if is_realtime_case:
chunk_stats = pop_realtime_perf_stats(case.id)
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
),
lambda: self._validate_realtime_performance(
diffusion_server,
case,
chunk_stats,
),
)
else:
@@ -51,6 +51,7 @@ from sglang.multimodal_gen.test.test_utils import (
get_video_frame_count,
is_image_url,
prepare_perf_log,
validate_audio_output,
validate_image,
validate_image_file,
validate_openai_video,
@@ -461,9 +462,7 @@ class ServerManager:
flush=True,
)
self._wait_for_ready(process, stdout_path)
return ServerContext(
context = ServerContext(
port=self.port,
process=process,
model=self.model,
@@ -473,6 +472,13 @@ class ServerManager:
_stdout_fh=stdout_fh,
_log_thread=log_thread,
)
try:
self._wait_for_ready(process, stdout_path)
except BaseException:
context.cleanup()
raise
return context
def _wait_for_ready(self, process: subprocess.Popen, stdout_path: Path) -> None:
"""Wait until model warmup finishes and inference traffic is accepted."""
@@ -539,7 +545,8 @@ class PerformanceValidator:
actual: float,
expected: float,
tolerance: float,
min_abs_tolerance_ms: float = 20.0,
min_abs_tolerance: float = 20.0,
unit: str = "ms",
):
"""Assert that actual is less than or equal to expected within a tolerance.
@@ -555,29 +562,54 @@ class PerformanceValidator:
# Use 100% higher tolerance for AMD (2x the expected value)
amd_tolerance = 1.0 # 100%
upper_bound = calculate_upper_bound(
expected, amd_tolerance, min_abs_tolerance_ms
expected, amd_tolerance, min_abs_tolerance
)
if actual > upper_bound:
logger.warning(
f"[AMD PERF WARNING] Validation would fail for '{name}'.\n"
f" Actual: {actual:.4f}ms\n"
f" Expected: {expected:.4f}ms\n"
f" AMD Limit: {upper_bound:.4f}ms "
f"(rel_tol: {amd_tolerance:.1%}, abs_pad: {min_abs_tolerance_ms}ms)\n"
f" Actual: {actual:.4f}{unit}\n"
f" Expected: {expected:.4f}{unit}\n"
f" AMD Limit: {upper_bound:.4f}{unit} "
f"(rel_tol: {amd_tolerance:.1%}, "
f"abs_pad: {min_abs_tolerance}{unit})\n"
f" Original tolerance was: {tolerance:.1%}"
)
else:
upper_bound = calculate_upper_bound(
expected, tolerance, min_abs_tolerance_ms
)
upper_bound = calculate_upper_bound(expected, tolerance, min_abs_tolerance)
assert actual <= upper_bound, (
f"Validation failed for '{name}'.\n"
f" Actual: {actual:.4f}ms\n"
f" Expected: {expected:.4f}ms\n"
f" Limit: {upper_bound:.4f}ms "
f"(rel_tol: {tolerance:.1%}, abs_pad: {min_abs_tolerance_ms}ms)"
f" Actual: {actual:.4f}{unit}\n"
f" Expected: {expected:.4f}{unit}\n"
f" Limit: {upper_bound:.4f}{unit} "
f"(rel_tol: {tolerance:.1%}, "
f"abs_pad: {min_abs_tolerance}{unit})"
)
def validate_peak_vram(
self,
summary: PerformanceSummary,
expected_load_peak_vram_mb: float,
expected_runtime_peak_vram_mb: float,
) -> None:
assert summary.load_peak_vram_mb > 0, "Load peak VRAM metric missing"
assert summary.runtime_peak_vram_mb > 0, "Runtime peak VRAM metric missing"
self._assert_le(
"Load Peak VRAM",
summary.load_peak_vram_mb,
expected_load_peak_vram_mb,
self.tolerances.load_peak_vram,
min_abs_tolerance=128.0,
unit=" MiB",
)
self._assert_le(
"Runtime Peak VRAM",
summary.runtime_peak_vram_mb,
expected_runtime_peak_vram_mb,
self.tolerances.runtime_peak_vram,
min_abs_tolerance=128.0,
unit=" MiB",
)
def validate(
self, perf_record: RequestPerfRecord, *args, **kwargs
) -> PerformanceSummary:
@@ -640,7 +672,7 @@ class PerformanceValidator:
actual,
expected,
FIRST_DENOISE_STEP_TOLERANCE,
min_abs_tolerance_ms=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
)
continue
@@ -667,15 +699,15 @@ class PerformanceValidator:
)
if stage.endswith("DecodingStage"):
tolerance = max(tolerance, 0.9)
min_abs_tolerance_ms = DECODING_STAGE_MIN_ABS_TOLERANCE_MS
min_abs_tolerance = DECODING_STAGE_MIN_ABS_TOLERANCE_MS
else:
min_abs_tolerance_ms = 120.0
min_abs_tolerance = 120.0
self._assert_le(
f"Stage '{stage}'",
actual,
expected,
tolerance,
min_abs_tolerance_ms=min_abs_tolerance_ms,
min_abs_tolerance=min_abs_tolerance,
)
@@ -696,7 +728,7 @@ class VideoPerformanceValidator(PerformanceValidator):
actual,
expected,
FIRST_DENOISE_STEP_TOLERANCE,
min_abs_tolerance_ms=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
)
continue
@@ -707,7 +739,7 @@ class VideoPerformanceValidator(PerformanceValidator):
actual,
expected,
self.tolerances.denoise_step,
min_abs_tolerance_ms=VIDEO_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
min_abs_tolerance=VIDEO_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
)
def validate(
@@ -1022,6 +1054,15 @@ def get_generate_fn(
validate_video_file(
tmp_path, expected_filename, expected_width, expected_height
)
if sampling_params.expect_audio_output:
audio_info = validate_audio_output(tmp_path)
logger.info(
"%s: validated audio output (%s Hz, %s channels, %.3fs)",
case_id,
audio_info.sample_rate,
audio_info.channels,
audio_info.duration_seconds,
)
if expected_frame_count is not None:
actual_count = get_video_frame_count(tmp_path)
@@ -1390,7 +1431,7 @@ def get_generate_fn(
init_payload=init_payload,
events=list(sampling_params.realtime_events),
num_chunks=sampling_params.realtime_num_chunks,
require_chunk_stats=bool(sampling_params.realtime_perf_thresholds),
require_chunk_stats=True,
)
)
record_realtime_perf_stats(case_id, realtime_output.chunk_stats)
@@ -46,6 +46,8 @@ class ToleranceConfig:
non_denoise_stage: float
denoise_step: float
denoise_agg: float
load_peak_vram: float = 0.01
runtime_peak_vram: float = 0.02
@classmethod
def load_profile(cls, all_tolerances: dict, profile_name: str) -> ToleranceConfig:
@@ -86,6 +88,18 @@ class ToleranceConfig:
denoise_agg=float(
os.getenv("SGLANG_DENOISE_AGG_TOLERANCE", tol_data["denoise_agg"])
),
load_peak_vram=float(
os.getenv(
"SGLANG_LOAD_PEAK_VRAM_TOLERANCE",
tol_data.get("load_peak_vram", 0.01),
)
),
runtime_peak_vram=float(
os.getenv(
"SGLANG_RUNTIME_PEAK_VRAM_TOLERANCE",
tol_data.get("runtime_peak_vram", 0.02),
)
),
)
@@ -99,6 +113,25 @@ class ScenarioConfig:
expected_avg_denoise_ms: float
expected_median_denoise_ms: float
estimated_full_test_time_s: float | None = None
load_peak_vram_mb: float | None = None
runtime_peak_vram_mb: float | None = None
@classmethod
def from_dict(cls, cfg: dict[str, Any]) -> ScenarioConfig:
def optional_float(name: str) -> float | None:
value = cfg.get(name)
return float(value) if value is not None else None
return cls(
stages_ms=cfg["stages_ms"],
denoise_step_ms={int(k): v for k, v in cfg["denoise_step_ms"].items()},
expected_e2e_ms=float(cfg["expected_e2e_ms"]),
expected_avg_denoise_ms=float(cfg["expected_avg_denoise_ms"]),
expected_median_denoise_ms=float(cfg["expected_median_denoise_ms"]),
estimated_full_test_time_s=optional_float("estimated_full_test_time_s"),
load_peak_vram_mb=optional_float("load_peak_vram_mb"),
runtime_peak_vram_mb=optional_float("runtime_peak_vram_mb"),
)
@dataclass
@@ -122,16 +155,10 @@ class BaselineConfig:
data.get("tolerances", {}), profile_name
)
scenarios = {}
for name, cfg in data["scenarios"].items():
scenarios[name] = ScenarioConfig(
stages_ms=cfg["stages_ms"],
denoise_step_ms={int(k): v for k, v in cfg["denoise_step_ms"].items()},
expected_e2e_ms=float(cfg["expected_e2e_ms"]),
expected_avg_denoise_ms=float(cfg["expected_avg_denoise_ms"]),
expected_median_denoise_ms=float(cfg["expected_median_denoise_ms"]),
estimated_full_test_time_s=cfg.get("estimated_full_test_time_s"),
)
scenarios = {
name: ScenarioConfig.from_dict(cfg)
for name, cfg in data["scenarios"].items()
}
return cls(
scenarios=scenarios,
@@ -147,18 +174,12 @@ class BaselineConfig:
with path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
scenarios_new = {}
for name, cfg in data["scenarios"].items():
scenarios_new[name] = ScenarioConfig(
stages_ms=cfg["stages_ms"],
denoise_step_ms={int(k): v for k, v in cfg["denoise_step_ms"].items()},
expected_e2e_ms=float(cfg["expected_e2e_ms"]),
expected_avg_denoise_ms=float(cfg["expected_avg_denoise_ms"]),
expected_median_denoise_ms=float(cfg["expected_median_denoise_ms"]),
estimated_full_test_time_s=cfg.get("estimated_full_test_time_s"),
)
self.scenarios.update(scenarios_new)
self.scenarios.update(
{
name: ScenarioConfig.from_dict(cfg)
for name, cfg in data["scenarios"].items()
}
)
return self
@@ -248,6 +269,7 @@ class DiffusionSamplingParams:
# output format
output_format: str | None = None # "png", "jpeg", "mp4", etc.
expect_audio_output: bool = False
num_outputs_per_prompt: int = 1
@@ -408,6 +430,8 @@ class PerformanceSummary:
step_metrics: list[float]
sampled_steps: dict[int, float]
all_denoise_steps: dict[int, float]
load_peak_vram_mb: float = 0.0
runtime_peak_vram_mb: float = 0.0
frames_per_second: float | None = None
total_frames: int | None = None
avg_frame_time_ms: float | None = None
@@ -437,6 +461,13 @@ class PerformanceSummary:
val = item.get("execution_time_ms", 0.0)
stage_metrics[item["name"]] = val
load_peak_vram_mb = float(
record.memory_snapshots.get("load_peak", {}).get("peak_reserved_mb", 0.0)
)
runtime_peak_vram_mb = float(
record.memory_snapshots.get("runtime_peak", {}).get("peak_reserved_mb", 0.0)
)
return PerformanceSummary(
e2e_ms=e2e_ms,
avg_denoise_ms=avg_denoise,
@@ -445,6 +476,8 @@ class PerformanceSummary:
step_metrics=step_durations,
sampled_steps=sampled_steps,
all_denoise_steps=per_step,
load_peak_vram_mb=load_peak_vram_mb,
runtime_peak_vram_mb=runtime_peak_vram_mb,
)
+451 -1
View File
@@ -10,6 +10,7 @@ import subprocess
import sys
import tempfile
import time
import wave
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -39,7 +40,7 @@ logger = init_logger(__name__)
# NPU/ascend) is read from sgl-project/ci-data-diffusion, where the GT-gen workflows
# publish.
SGL_TEST_FILES_CI_DATA_REPO = "sgl-project/ci-data-diffusion"
SGL_TEST_FILES_CI_DATA_REVISION = "8c3896984319c8d5628bf08df4b596baf2368ec7"
SGL_TEST_FILES_CI_DATA_REVISION = "15b30030ef980756788ab40072f9223fe21a5526"
# The NPU pin is kept as a separate branch so ascend GT can be bumped independently
# when it's regenerated on its own cadence.
@@ -100,6 +101,11 @@ DEFAULT_MEAN_ABS_DIFF_THRESHOLD_IMAGE = 8.0
DEFAULT_SSIM_THRESHOLD_VIDEO = 0.92
DEFAULT_PSNR_THRESHOLD_VIDEO = 24.0
DEFAULT_MEAN_ABS_DIFF_THRESHOLD_VIDEO = 10.0
AUDIO_CONSISTENCY_SAMPLE_RATE = 16_000
DEFAULT_AUDIO_SPECTRAL_SIMILARITY_THRESHOLD = 0.95
DEFAULT_AUDIO_WAVEFORM_CORRELATION_THRESHOLD = 0.90
DEFAULT_AUDIO_RMS_DB_DIFF_THRESHOLD = 2.0
DEFAULT_AUDIO_DURATION_DIFF_THRESHOLD = 0.10
_clip_model_cache: dict[str, Any] = {}
_consistency_gt_cache: dict[str, Any] = {}
_official_consistency_gt_outputs_cache: dict[str, frozenset[str]] | None = None
@@ -721,6 +727,162 @@ def validate_video_file(
), f"Video height mismatch: expected {expected_height}, got {actual_height}"
@dataclass(frozen=True)
class AudioStreamInfo:
sample_rate: int
channels: int
duration_seconds: float
def probe_audio_stream(file_path: str) -> AudioStreamInfo:
"""Return metadata for the first audio stream in a media file."""
try:
result = subprocess.run(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"stream=codec_type,sample_rate,channels,duration:format=duration",
"-of",
"json",
file_path,
],
check=True,
capture_output=True,
text=True,
)
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
stderr = getattr(exc, "stderr", None)
raise AssertionError(
f"Unable to inspect audio stream in {file_path}: {stderr or exc}"
) from exc
payload = json.loads(result.stdout)
stream = next(
(
item
for item in payload.get("streams", [])
if item.get("codec_type") == "audio"
),
None,
)
assert stream is not None, f"Media file has no audio stream: {file_path}"
sample_rate = int(stream.get("sample_rate") or 0)
channels = int(stream.get("channels") or 0)
duration = float(
stream.get("duration") or payload.get("format", {}).get("duration") or 0.0
)
assert sample_rate > 0, f"Audio stream has invalid sample rate: {sample_rate}"
assert channels > 0, f"Audio stream has invalid channel count: {channels}"
assert (
math.isfinite(duration) and duration > 0
), f"Audio stream has invalid duration: {duration}"
return AudioStreamInfo(sample_rate, channels, duration)
def extract_audio_pcm(
file_path: str,
sample_rate: int = AUDIO_CONSISTENCY_SAMPLE_RATE,
) -> np.ndarray:
"""Decode the first audio stream as mono float32 PCM."""
try:
result = subprocess.run(
[
"ffmpeg",
"-v",
"error",
"-i",
file_path,
"-map",
"0:a:0",
"-vn",
"-ac",
"1",
"-ar",
str(sample_rate),
"-f",
"f32le",
"pipe:1",
],
check=True,
capture_output=True,
)
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
stderr = getattr(exc, "stderr", b"")
if isinstance(stderr, bytes):
stderr = stderr.decode("utf-8", errors="replace")
raise AssertionError(
f"Unable to decode audio stream in {file_path}: {stderr or exc}"
) from exc
return np.frombuffer(result.stdout, dtype="<f4").copy()
def extract_audio_pcm_from_video_bytes(
video_bytes: bytes,
sample_rate: int = AUDIO_CONSISTENCY_SAMPLE_RATE,
) -> np.ndarray:
with tempfile.NamedTemporaryFile(suffix=".mp4") as tmp:
tmp.write(video_bytes)
tmp.flush()
return extract_audio_pcm(tmp.name, sample_rate)
def validate_audio_output(file_path: str) -> AudioStreamInfo:
"""Validate that a media file contains finite, non-silent audio."""
info = probe_audio_stream(file_path)
audio = extract_audio_pcm(file_path)
assert audio.size > 0, f"Decoded audio stream is empty: {file_path}"
assert np.isfinite(audio).all(), f"Decoded audio contains NaN or inf: {file_path}"
rms = float(np.sqrt(np.mean(np.square(audio, dtype=np.float64))))
assert rms > 1e-5, f"Decoded audio is silent or near-silent: rms={rms:.3e}"
return info
def encode_audio_gt_wav(
audio: np.ndarray,
sample_rate: int = AUDIO_CONSISTENCY_SAMPLE_RATE,
) -> bytes:
pcm = np.round(np.clip(audio, -1.0, 1.0) * 32767.0).astype("<i2")
output = io.BytesIO()
with wave.open(output, "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(sample_rate)
wav.writeframes(pcm.tobytes())
return output.getvalue()
def save_audio_gt_artifact(
artifact_dir: str | None,
case_id: str,
num_gpus: int,
audio: np.ndarray,
) -> Path | None:
if not artifact_dir:
return None
path = Path(artifact_dir) / f"{case_id}_{num_gpus}gpu_audio.wav"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(encode_audio_gt_wav(audio))
return path
def decode_audio_gt_wav(
content: bytes,
expected_sample_rate: int = AUDIO_CONSISTENCY_SAMPLE_RATE,
) -> np.ndarray:
with wave.open(io.BytesIO(content), "rb") as wav:
assert wav.getnchannels() == 1, "Audio GT must be mono"
assert wav.getsampwidth() == 2, "Audio GT must use signed 16-bit PCM"
assert wav.getframerate() == expected_sample_rate, (
f"Audio GT sample rate must be {expected_sample_rate}, "
f"got {wav.getframerate()}"
)
pcm = np.frombuffer(wav.readframes(wav.getnframes()), dtype="<i2")
return pcm.astype(np.float32) / 32767.0
def _normalize_consistency_platform(platform: str) -> str:
normalized = platform.strip().lower().replace("_", "-")
normalized = normalized.replace("-", "")
@@ -797,6 +959,24 @@ class ConsistencyThresholds:
mean_abs_diff_threshold: float
@dataclass(frozen=True)
class AudioConsistencyThresholds:
spectral_similarity_threshold: float
waveform_correlation_threshold: float
rms_db_diff_threshold: float
duration_diff_threshold: float
@dataclass(frozen=True)
class AudioConsistencyResult:
passed: bool
spectral_similarity: float
waveform_correlation: float
rms_db_diff: float
duration_diff: float
thresholds: AudioConsistencyThresholds
def get_consistency_thresholds(
case_id: str,
is_video: bool,
@@ -850,6 +1030,130 @@ def get_consistency_thresholds(
)
def get_audio_consistency_thresholds(
case_id: str,
metadata: dict[str, Any] | None = None,
) -> AudioConsistencyThresholds:
if metadata is None:
metadata = _load_threshold_json()
case_meta = metadata.get("cases", {}).get(case_id, {})
return AudioConsistencyThresholds(
spectral_similarity_threshold=float(
case_meta.get(
"audio_spectral_similarity_threshold",
metadata.get(
"default_audio_spectral_similarity_threshold",
DEFAULT_AUDIO_SPECTRAL_SIMILARITY_THRESHOLD,
),
)
),
waveform_correlation_threshold=float(
case_meta.get(
"audio_waveform_correlation_threshold",
metadata.get(
"default_audio_waveform_correlation_threshold",
DEFAULT_AUDIO_WAVEFORM_CORRELATION_THRESHOLD,
),
)
),
rms_db_diff_threshold=float(
case_meta.get(
"audio_rms_db_diff_threshold",
metadata.get(
"default_audio_rms_db_diff_threshold",
DEFAULT_AUDIO_RMS_DB_DIFF_THRESHOLD,
),
)
),
duration_diff_threshold=float(
case_meta.get(
"audio_duration_diff_threshold",
metadata.get(
"default_audio_duration_diff_threshold",
DEFAULT_AUDIO_DURATION_DIFF_THRESHOLD,
),
)
),
)
def _audio_magnitude_spectrogram(audio: np.ndarray) -> np.ndarray:
frame_length = 512
hop_length = 160
if audio.size < frame_length:
audio = np.pad(audio, (0, frame_length - audio.size))
frame_count = 1 + (audio.size - frame_length) // hop_length
frames = np.lib.stride_tricks.sliding_window_view(audio, frame_length)[
: frame_count * hop_length : hop_length
]
window = np.hanning(frame_length).astype(np.float32)
magnitude = np.abs(np.fft.rfft(frames * window, axis=1))
return np.sqrt(magnitude).astype(np.float32, copy=False)
def compare_audio_with_gt(
output_audio: np.ndarray,
gt_audio: np.ndarray,
thresholds: AudioConsistencyThresholds,
sample_rate: int = AUDIO_CONSISTENCY_SAMPLE_RATE,
) -> AudioConsistencyResult:
output_audio = np.asarray(output_audio, dtype=np.float32).reshape(-1)
gt_audio = np.asarray(gt_audio, dtype=np.float32).reshape(-1)
if output_audio.size == 0 or gt_audio.size == 0:
raise ValueError("Audio consistency inputs must be non-empty")
if not np.isfinite(output_audio).all() or not np.isfinite(gt_audio).all():
raise ValueError("Audio consistency inputs must be finite")
duration_diff = abs(output_audio.size - gt_audio.size) / float(sample_rate)
sample_count = min(output_audio.size, gt_audio.size)
output = output_audio[:sample_count]
target = gt_audio[:sample_count]
output_centered = output - float(output.mean())
target_centered = target - float(target.mean())
correlation_denominator = float(
np.linalg.norm(output_centered) * np.linalg.norm(target_centered)
)
waveform_correlation = (
float(np.dot(output_centered, target_centered)) / correlation_denominator
if correlation_denominator > 0
else 0.0
)
output_spectrum = _audio_magnitude_spectrogram(output)
target_spectrum = _audio_magnitude_spectrogram(target)
spectral_denominator = float(
np.linalg.norm(output_spectrum) * np.linalg.norm(target_spectrum)
)
spectral_similarity = (
float(np.vdot(output_spectrum, target_spectrum)) / spectral_denominator
if spectral_denominator > 0
else 0.0
)
output_rms = float(np.sqrt(np.mean(np.square(output, dtype=np.float64))))
target_rms = float(np.sqrt(np.mean(np.square(target, dtype=np.float64))))
rms_db_diff = abs(
20.0 * math.log10(max(output_rms, 1e-12))
- 20.0 * math.log10(max(target_rms, 1e-12))
)
passed = (
spectral_similarity >= thresholds.spectral_similarity_threshold
and waveform_correlation >= thresholds.waveform_correlation_threshold
and rms_db_diff <= thresholds.rms_db_diff_threshold
and duration_diff <= thresholds.duration_diff_threshold
)
return AudioConsistencyResult(
passed=passed,
spectral_similarity=spectral_similarity,
waveform_correlation=waveform_correlation,
rms_db_diff=rms_db_diff,
duration_diff=duration_diff,
thresholds=thresholds,
)
def get_clip_threshold(
case: "DiffusionTestCase",
metadata: dict[str, Any] | None = None,
@@ -1115,6 +1419,30 @@ def get_action_consistency_gt_candidates(case_id: str, num_gpus: int) -> list[st
]
def _audio_consistency_gt_filenames(case_id: str, num_gpus: int) -> list[str]:
case_id = get_consistency_gt_case_id(case_id)
return [f"{case_id}_{num_gpus}gpu_audio.wav"]
def get_audio_consistency_gt_candidate_sets(
case_id: str,
num_gpus: int,
) -> list[list[str]]:
candidates = _audio_consistency_gt_filenames(case_id, num_gpus)
if _is_ascend_consistency_case(case_id) or current_platform.is_npu():
return [candidates]
platform = get_consistency_platform()
return [[f"{platform}/{candidate}" for candidate in candidates], candidates]
def get_audio_consistency_gt_candidates(case_id: str, num_gpus: int) -> list[str]:
return [
candidate
for candidate_set in get_audio_consistency_gt_candidate_sets(case_id, num_gpus)
for candidate in candidate_set
]
def get_consistency_gt_remote_files(
case_id: str, num_gpus: int, is_video: bool, output_format: str | None = None
) -> list[tuple[str, str]]:
@@ -1143,6 +1471,19 @@ def get_action_consistency_gt_remote_files(
]
def get_audio_consistency_gt_remote_files(
case_id: str, num_gpus: int
) -> list[tuple[str, str]]:
files = _find_remote_audio_consistency_gt_files(case_id, num_gpus)
if files:
return files
filenames = get_audio_consistency_gt_candidates(case_id, num_gpus)
return [
(filename, f"{SGL_TEST_FILES_CONSISTENCY_GT_BASE}/{filename}")
for filename in filenames
]
def _remote_consistency_gt_candidates(
base_url: str,
case_id: str,
@@ -1375,6 +1716,35 @@ def _find_remote_action_consistency_gt_files(
return []
def _find_remote_audio_consistency_gt_files(
case_id: str,
num_gpus: int,
) -> list[tuple[str, str]]:
for filenames in get_audio_consistency_gt_candidate_sets(case_id, num_gpus):
for base_url in _remote_consistency_gt_base_urls(case_id):
candidates = [
(filename, f"{base_url}/{filename}") for filename in filenames
]
if _is_official_consistency_gt_base_url(base_url):
candidates = [
(filename, url)
for filename, url in candidates
if _official_consistency_gt_candidate_is_declared(case_id, filename)
]
if not candidates:
continue
uncertain_candidate = None
for filename, url in candidates:
exists = _remote_file_exists(url)
if exists is True:
return [(filename, url)]
if exists is None and uncertain_candidate is None:
uncertain_candidate = (filename, url)
if uncertain_candidate is not None:
return [uncertain_candidate]
return []
def _get_consistency_gt_dir() -> Path | None:
"""Return the local GT directory when configured."""
d = os.environ.get("SGLANG_CONSISTENCY_GT_DIR")
@@ -1402,6 +1772,13 @@ def _get_action_consistency_gt_cache_key(case_id: str, num_gpus: int) -> str:
return f"{platform}:{case_id}:{num_gpus}:action:{source}"
def _get_audio_consistency_gt_cache_key(case_id: str, num_gpus: int) -> str:
gt_dir = _get_consistency_gt_dir()
source = str(gt_dir) if gt_dir is not None else "remote"
platform = get_consistency_platform()
return f"{platform}:{case_id}:{num_gpus}:audio:{source}"
def load_consistency_gt(
case_id: str,
num_gpus: int,
@@ -1534,6 +1911,62 @@ def load_action_consistency_gt(case_id: str, num_gpus: int) -> dict[str, Any]:
return loaded_gt
def _load_remote_gt_bytes(url: str) -> bytes:
last_error: Exception | None = None
for _ in range(3):
try:
resp = requests.get(url, timeout=60)
try:
if resp.status_code == 200:
return resp.content
last_error = FileNotFoundError(f"GT file not found: {url}")
if resp.status_code not in (403, 429) and resp.status_code < 500:
break
finally:
resp.close()
except requests.RequestException as exc:
last_error = exc
raise FileNotFoundError(f"GT file not found: {url}") from last_error
def load_audio_consistency_gt(case_id: str, num_gpus: int) -> np.ndarray:
cache_key = _get_audio_consistency_gt_cache_key(case_id, num_gpus)
cached = _consistency_gt_cache.get(cache_key)
if cached is not None:
return cached
gt_dir = _get_consistency_gt_dir()
if gt_dir is not None:
path = next(
(
gt_dir / filename
for filename in get_audio_consistency_gt_candidates(case_id, num_gpus)
if (gt_dir / filename).exists()
),
None,
)
if path is None:
candidates = get_audio_consistency_gt_candidates(case_id, num_gpus)
raise FileNotFoundError(
f"GT audio not found in {gt_dir}. Tried: {', '.join(candidates)}"
)
content = path.read_bytes()
logger.info("Loaded audio GT for %s from %s", case_id, path)
else:
remote_files = _find_remote_audio_consistency_gt_files(case_id, num_gpus)
if not remote_files:
candidates = get_audio_consistency_gt_candidates(case_id, num_gpus)
raise FileNotFoundError(
f"GT audio not found for {case_id}. Tried: {', '.join(candidates)}"
)
content = _load_remote_gt_bytes(remote_files[0][1])
logger.info("Loaded audio GT for %s from %s", case_id, remote_files[0][1])
loaded_gt = decode_audio_gt_wav(content)
_consistency_gt_cache[cache_key] = loaded_gt
return loaded_gt
def load_gt_embeddings(
case_id: str,
num_gpus: int,
@@ -1585,6 +2018,23 @@ def gt_exists(
return found
def audio_gt_exists(case_id: str, num_gpus: int) -> bool:
gt_dir = _get_consistency_gt_dir()
if gt_dir is not None:
return any(
(gt_dir / candidate).exists()
for candidate in get_audio_consistency_gt_candidates(case_id, num_gpus)
)
cache_key = _get_audio_consistency_gt_cache_key(case_id, num_gpus)
if cache_key in _gt_exists_remote_cache:
return True
found = bool(_find_remote_audio_consistency_gt_files(case_id, num_gpus))
if found:
_gt_exists_remote_cache.add(cache_key)
return found
def action_gt_exists(case_id: str, num_gpus: int) -> bool:
gt_dir = _get_consistency_gt_dir()
if gt_dir is not None:
@@ -1,4 +1,5 @@
import math
from types import SimpleNamespace
import numpy as np
import pytest
@@ -625,3 +626,107 @@ def test_save_consistency_failure_artifact(tmp_path, monkeypatch):
assert (
tmp_path / "consistency_failures" / "generated" / "unit_image_fail_1gpu.png"
).exists()
def _sine_wave(frequency: float, seconds: float = 1.0) -> np.ndarray:
t = np.arange(
int(test_utils.AUDIO_CONSISTENCY_SAMPLE_RATE * seconds), dtype=np.float32
)
return np.sin(
2.0 * np.pi * frequency * t / float(test_utils.AUDIO_CONSISTENCY_SAMPLE_RATE)
).astype(np.float32)
def _audio_thresholds() -> test_utils.AudioConsistencyThresholds:
return test_utils.AudioConsistencyThresholds(
spectral_similarity_threshold=0.95,
waveform_correlation_threshold=0.90,
rms_db_diff_threshold=1.0,
duration_diff_threshold=0.05,
)
def test_audio_consistency_wav_round_trip_passes():
output = _sine_wave(440.0)
gt = test_utils.decode_audio_gt_wav(test_utils.encode_audio_gt_wav(output))
result = test_utils.compare_audio_with_gt(output, gt, _audio_thresholds())
assert result.passed
assert result.spectral_similarity > 0.999
assert result.waveform_correlation > 0.999
assert result.rms_db_diff < 0.001
assert result.duration_diff == 0.0
def test_save_audio_gt_artifact(tmp_path):
path = test_utils.save_audio_gt_artifact(
str(tmp_path), "unit_audio", 2, _sine_wave(440.0)
)
assert path == tmp_path / "unit_audio_2gpu_audio.wav"
assert test_utils.decode_audio_gt_wav(path.read_bytes()).size > 0
def test_audio_consistency_rejects_wrong_audio():
result = test_utils.compare_audio_with_gt(
_sine_wave(440.0),
_sine_wave(880.0),
_audio_thresholds(),
)
assert not result.passed
assert result.spectral_similarity < 0.95
assert result.waveform_correlation < 0.90
def test_audio_gt_candidates_prefer_platform_directory(monkeypatch):
monkeypatch.setenv(test_utils.CONSISTENCY_PLATFORM_ENV, "h100")
sglang_prefix = test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE + "/"
monkeypatch.setattr(
test_utils,
"_remote_file_exists",
lambda url: url.startswith(sglang_prefix),
)
files = test_utils._find_remote_audio_consistency_gt_files("unit_audio", 2)
assert files == [
(
"h100/unit_audio_2gpu_audio.wav",
f"{sglang_prefix}h100/unit_audio_2gpu_audio.wav",
)
]
def test_probe_audio_stream_reads_positive_metadata(monkeypatch):
monkeypatch.setattr(
test_utils.subprocess,
"run",
lambda *args, **kwargs: SimpleNamespace(
stdout=(
'{"streams":[{"codec_type":"audio","sample_rate":"32000",'
'"channels":2,"duration":"4.0"}],"format":{"duration":"4.1"}}'
)
),
)
info = test_utils.probe_audio_stream("output.mp4")
assert info == test_utils.AudioStreamInfo(32000, 2, 4.0)
def test_validate_audio_output_rejects_silence(monkeypatch):
monkeypatch.setattr(
test_utils,
"probe_audio_stream",
lambda path: test_utils.AudioStreamInfo(32000, 2, 4.0),
)
monkeypatch.setattr(
test_utils,
"extract_audio_pcm",
lambda path: np.zeros(test_utils.AUDIO_CONSISTENCY_SAMPLE_RATE),
)
with pytest.raises(AssertionError, match="silent"):
test_utils.validate_audio_output("output.mp4")
@@ -1,7 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
"""Unit tests for disaggregation role-based module filtering."""
import json
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
@@ -244,6 +247,39 @@ class _FakeStage:
class TestPipelineStageRoleFilter(unittest.TestCase):
def test_load_config_passes_server_revision_to_model_download(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
(tmp_path / "model_index.json").write_text(
json.dumps(
{
"_class_name": "FakePipeline",
"_diffusers_version": "0.34.0",
"transformer": ["diffusers", "FakeTransformer"],
}
)
)
(tmp_path / "transformer").mkdir()
pipeline = object.__new__(_FakePipeline)
pipeline.model_path = "org/repo"
pipeline.server_args = SimpleNamespace(
model_subfolder=None,
revision="a" * 40,
)
with patch(
"sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base.maybe_download_model",
return_value=str(tmp_path),
) as download_model:
pipeline._load_config()
download_model.assert_called_once_with(
"org/repo",
force_diffusers_model=True,
revision="a" * 40,
)
def test_stage_factory_skips_without_constructing_for_other_role(self):
pipeline = _make_pipeline(RoleType.ENCODER)
@@ -134,6 +134,38 @@ class TestServerManagerReadiness(unittest.TestCase):
["http://127.0.0.1:11000/health"] * 2,
)
def test_start_cleans_up_process_when_readiness_fails(self):
manager = ServerManager("test-model", port=11000, wait_deadline=1)
process = SimpleNamespace(pid=123, stdout=None)
with tempfile.TemporaryDirectory() as temp_dir:
log_dir = Path(temp_dir)
with (
mock.patch(
"sglang.multimodal_gen.test.server.test_server_utils.prepare_perf_log",
return_value=(log_dir, log_dir / "perf.jsonl"),
),
mock.patch(
"sglang.multimodal_gen.test.server.test_server_utils.subprocess.Popen",
return_value=process,
),
mock.patch.object(
manager,
"_wait_for_ready",
side_effect=TimeoutError("startup timed out"),
),
mock.patch(
"sglang.multimodal_gen.test.server.test_server_utils.kill_process_tree"
) as kill_process,
mock.patch(
"sglang.multimodal_gen.test.server.test_server_utils.time.sleep"
),
):
with self.assertRaisesRegex(TimeoutError, "startup timed out"):
manager.start()
kill_process.assert_called_once_with(123)
if __name__ == "__main__":
unittest.main()
@@ -204,6 +204,31 @@ def test_complete_cached_snapshot_is_served_without_download(
assert calls == ["probe"]
def test_cached_snapshot_respects_requested_revision(monkeypatch, tmp_path):
calls = []
def fake_snapshot_download(**kwargs):
calls.append(kwargs)
return str(tmp_path)
monkeypatch.setattr(hf_diffusers_utils, "snapshot_download", fake_snapshot_download)
assert maybe_download_model("org/repo", revision="a" * 40, download=False) == str(
tmp_path
)
assert calls == [
{
"repo_id": "org/repo",
"ignore_patterns": ["*.onnx", "*.msgpack"],
"allow_patterns": None,
"local_dir": None,
"local_files_only": True,
"max_workers": 8,
"revision": "a" * 40,
}
]
def test_partially_populated_cached_snapshot_is_served_without_download(
recording_snapshot_download, tmp_path
):
@@ -11,6 +11,7 @@ import numpy as np
import pytest
import torch
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3 import (
material_io,
reference_encoding,
@@ -256,3 +257,42 @@ def test_audio_decode_is_bounded_float_pcm_without_temp_files(monkeypatch):
assert ffmpeg[ffmpeg.index("-ss") + 1] == "2.25"
assert ffmpeg.index("-ss") < ffmpeg.index("-i")
assert ffmpeg[-3:] == ["-f", "f32le", "pipe:1"]
def test_reference_audio_encode_sets_forward_context(monkeypatch):
class FakeAudioVAE(torch.nn.Module):
attn_proj = True
def __init__(self):
super().__init__()
self.anchor = torch.nn.Parameter(torch.zeros(1))
self.mean_proj = torch.nn.Identity()
def preprocess(self, waveform, _sample_rate):
return waveform
def encoder(self, _audio_data):
return torch.ones(2, 32, 4)
def pre_block(self, hidden_states):
assert get_forward_context().current_timestep == 0
return hidden_states
monkeypatch.setattr(
reference_encoding,
"_load_waveform",
lambda *_args, **_kwargs: (torch.ones(2, 320), 32000),
)
result = reference_encoding.minimax_h3_encode_reference_audio_rows(
FakeAudioVAE(),
"/input/ref.wav",
SimpleNamespace(
latent_channels=32,
latents_mean=[0.0] * 32,
latents_std=[1.0] * 32,
),
)
assert result["rows"].shape == (8, 32)
assert result["ref_audio_t"] == 4
@@ -0,0 +1,235 @@
import json
from unittest.mock import Mock, patch
import pytest
import torch
import sglang.multimodal_gen.runtime.managers.gpu_worker as gpu_worker_module
from sglang.multimodal_gen.runtime.managers.gpu_worker import GPUWorker
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.perf_logger import (
MemorySnapshot,
RequestMetrics,
RequestPerfRecord,
)
from sglang.multimodal_gen.test.server.conftest import _write_results_json
from sglang.multimodal_gen.test.server.test_server_utils import PerformanceValidator
from sglang.multimodal_gen.test.server.testcase_configs import (
BaselineConfig,
PerformanceSummary,
ScenarioConfig,
ToleranceConfig,
)
def _perf_record(memory_snapshots: dict[str, dict]) -> RequestPerfRecord:
return RequestPerfRecord(
request_id="request",
commit_hash="commit",
tag="test",
stages=[],
steps=[],
total_duration_ms=1.0,
memory_snapshots=memory_snapshots,
)
def test_performance_summary_separates_load_and_runtime_peaks():
summary = PerformanceSummary.from_req_perf_record(
_perf_record(
{
"after_forward": {"peak_reserved_mb": 1024.0},
"load_peak": {"peak_reserved_mb": 4096.0},
"runtime_peak": {"peak_reserved_mb": 3072.0},
}
),
step_fractions=(),
)
assert summary.load_peak_vram_mb == 4096.0
assert summary.runtime_peak_vram_mb == 3072.0
def test_worker_records_replica_load_and_runtime_peaks():
worker = GPUWorker.__new__(GPUWorker)
worker.local_rank = 0
worker.is_output_rank = True
worker._load_peak_reserved_mb = 4096.0
worker._runtime_peak_reserved_mb = 0.0
output = OutputBatch()
metrics = RequestMetrics("request")
replica_group = Mock()
replica_group.all_reduce.return_value = torch.tensor(
[5120.0, 3584.0], dtype=torch.float64
)
snapshots = [
MemorySnapshot(0.0, 0.0, 2048.0, 3072.0),
MemorySnapshot(0.0, 0.0, 2048.0, 3072.0),
]
with (
patch.object(current_platform, "is_cpu", return_value=False),
patch.object(current_platform, "is_cuda", return_value=True),
patch.object(current_platform, "get_device", return_value=torch.device("cpu")),
patch.object(
gpu_worker_module, "capture_memory_snapshot", side_effect=snapshots
),
patch.object(
gpu_worker_module, "get_replica_group", return_value=replica_group
),
):
worker._record_output_peak_memory(output)
worker._record_replica_peak_memory([metrics])
assert output.peak_memory_mb == 3072.0
assert worker._load_peak_reserved_mb == 4096.0
assert worker._runtime_peak_reserved_mb == 3072.0
assert metrics.memory_snapshots["load_peak"].peak_reserved_mb == 5120.0
assert metrics.memory_snapshots["runtime_peak"].peak_reserved_mb == 3584.0
def test_baseline_config_loads_per_scenario_peak_vram(tmp_path):
path = tmp_path / "baseline.json"
path.write_text(
json.dumps(
{
"tolerances": {
"pr_test": {
"e2e": 0.25,
"denoise_stage": 0.25,
"non_denoise_stage": 0.8,
"denoise_step": 0.3,
"denoise_agg": 0.2,
"load_peak_vram": 0.01,
"runtime_peak_vram": 0.02,
}
},
"sampling": {"step_fractions": [0.0, 1.0]},
"scenarios": {
"case": {
"stages_ms": {},
"denoise_step_ms": {},
"expected_e2e_ms": 1.0,
"expected_avg_denoise_ms": 1.0,
"expected_median_denoise_ms": 1.0,
"load_peak_vram_mb": 1234.5,
"runtime_peak_vram_mb": 2345.6,
}
},
}
),
encoding="utf-8",
)
config = BaselineConfig.load(path)
scenario = config.scenarios["case"]
assert scenario.load_peak_vram_mb == 1234.5
assert scenario.runtime_peak_vram_mb == 2345.6
assert config.tolerances.load_peak_vram == 0.01
assert config.tolerances.runtime_peak_vram == 0.02
def test_peak_vram_validation_uses_independent_tolerances():
validator = PerformanceValidator(
scenario=ScenarioConfig({}, {}, 0.0, 0.0, 0.0),
tolerances=ToleranceConfig(
0.0,
0.0,
0.0,
0.0,
0.0,
load_peak_vram=0.01,
runtime_peak_vram=0.02,
),
step_fractions=(),
)
load_regression = PerformanceSummary(
0.0,
0.0,
0.0,
{},
[],
{},
{},
load_peak_vram_mb=10_129.0,
runtime_peak_vram_mb=10_000.0,
)
runtime_regression = PerformanceSummary(
0.0,
0.0,
0.0,
{},
[],
{},
{},
load_peak_vram_mb=10_000.0,
runtime_peak_vram_mb=10_201.0,
)
with patch.object(current_platform, "is_hip", return_value=False):
with pytest.raises(AssertionError, match="Load Peak VRAM"):
validator.validate_peak_vram(load_regression, 10_000.0, 10_000.0)
with pytest.raises(AssertionError, match="Runtime Peak VRAM"):
validator.validate_peak_vram(runtime_regression, 10_000.0, 10_000.0)
@pytest.mark.parametrize(
("load_peak_vram_mb", "runtime_peak_vram_mb", "message"),
[
(0.0, 10_000.0, "Load peak VRAM metric missing"),
(10_000.0, 0.0, "Runtime peak VRAM metric missing"),
],
)
def test_peak_vram_validation_rejects_missing_metrics(
load_peak_vram_mb, runtime_peak_vram_mb, message
):
validator = PerformanceValidator(
scenario=ScenarioConfig({}, {}, 0.0, 0.0, 0.0),
tolerances=ToleranceConfig(0.0, 0.0, 0.0, 0.0, 0.0),
step_fractions=(),
)
summary = PerformanceSummary(
0.0,
0.0,
0.0,
{},
[],
{},
{},
load_peak_vram_mb=load_peak_vram_mb,
runtime_peak_vram_mb=runtime_peak_vram_mb,
)
with pytest.raises(AssertionError, match=message):
validator.validate_peak_vram(summary, 10_000.0, 10_000.0)
def test_results_json_merges_retry_sessions(tmp_path):
path = tmp_path / "diffusion-results.json"
_write_results_json(
[
{
"class_name": "Suite",
"test_name": "first",
"load_peak_vram_mb": 1.0,
"runtime_peak_vram_mb": 2.0,
}
],
str(path),
)
_write_results_json(
[
{
"class_name": "Suite",
"test_name": "second",
"load_peak_vram_mb": 3.0,
"runtime_peak_vram_mb": 4.0,
}
],
str(path),
)
results = json.loads(path.read_text(encoding="utf-8"))
assert {item["test_name"] for item in results} == {"first", "second"}