[diffusion] chore: make nightly performance measurements robust (#37915)
This commit is contained in:
@@ -424,6 +424,7 @@ async def edits(
|
|||||||
enable_upscaling: Optional[bool] = Form(False),
|
enable_upscaling: Optional[bool] = Form(False),
|
||||||
upscaling_model_path: Optional[str] = Form(None),
|
upscaling_model_path: Optional[str] = Form(None),
|
||||||
upscaling_scale: Optional[int] = Form(4),
|
upscaling_scale: Optional[int] = Form(4),
|
||||||
|
perf_dump_path: Optional[str] = Form(None),
|
||||||
num_frames: int = Form(1),
|
num_frames: int = Form(1),
|
||||||
):
|
):
|
||||||
request_id = generate_request_id()
|
request_id = generate_request_id()
|
||||||
@@ -484,6 +485,7 @@ async def edits(
|
|||||||
enable_upscaling=enable_upscaling,
|
enable_upscaling=enable_upscaling,
|
||||||
upscaling_model_path=upscaling_model_path,
|
upscaling_model_path=upscaling_model_path,
|
||||||
upscaling_scale=upscaling_scale,
|
upscaling_scale=upscaling_scale,
|
||||||
|
perf_dump_path=perf_dump_path,
|
||||||
)
|
)
|
||||||
trace_headers = extract_trace_headers(raw_request.headers)
|
trace_headers = extract_trace_headers(raw_request.headers)
|
||||||
batch = prepare_request(
|
batch = prepare_request(
|
||||||
|
|||||||
@@ -494,6 +494,7 @@ async def create_video(
|
|||||||
output_quality: Optional[str] = Form(None),
|
output_quality: Optional[str] = Form(None),
|
||||||
output_compression: Optional[int] = Form(None),
|
output_compression: Optional[int] = Form(None),
|
||||||
output_path: Optional[str] = Form(None),
|
output_path: Optional[str] = Form(None),
|
||||||
|
perf_dump_path: Optional[str] = Form(None),
|
||||||
extra_params: Optional[str] = Form(None),
|
extra_params: Optional[str] = Form(None),
|
||||||
extra_body: Optional[str] = Form(None),
|
extra_body: Optional[str] = Form(None),
|
||||||
):
|
):
|
||||||
@@ -645,6 +646,7 @@ async def create_video(
|
|||||||
output_compression=form_value("output_compression", output_compression),
|
output_compression=form_value("output_compression", output_compression),
|
||||||
output_quality=form_value("output_quality", output_quality),
|
output_quality=form_value("output_quality", output_quality),
|
||||||
output_path=form_value("output_path", output_path),
|
output_path=form_value("output_path", output_path),
|
||||||
|
perf_dump_path=form_value("perf_dump_path", perf_dump_path),
|
||||||
diffusers_kwargs=form_value("diffusers_kwargs", None),
|
diffusers_kwargs=form_value("diffusers_kwargs", None),
|
||||||
**extra_request_fields,
|
**extra_request_fields,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[5]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_script(name: str, relative_path: str):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relative_path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
runner = _load_script(
|
||||||
|
"diffusion_nightly_runner",
|
||||||
|
"scripts/ci/utils/diffusion/run_comparison.py",
|
||||||
|
)
|
||||||
|
dashboard = _load_script(
|
||||||
|
"diffusion_nightly_dashboard",
|
||||||
|
"scripts/ci/utils/diffusion/generate_diffusion_dashboard.py",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sglang_server_warmup_matches_measured_shape():
|
||||||
|
case = {
|
||||||
|
"model": "example/model",
|
||||||
|
"num_gpus": 2,
|
||||||
|
"width": 768,
|
||||||
|
"height": 512,
|
||||||
|
"num_frames": 121,
|
||||||
|
}
|
||||||
|
command = runner._build_sglang_cmd(
|
||||||
|
case,
|
||||||
|
{"serve_args": "--warmup-mode server --tp-size 2"},
|
||||||
|
30000,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolution_index = command.index("--warmup-resolutions")
|
||||||
|
frame_index = command.index("--warmup-num-frames")
|
||||||
|
assert command[resolution_index + 1] == "768x512"
|
||||||
|
assert command[frame_index + 1] == "121"
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_server_warmup_shape_is_preserved():
|
||||||
|
case = {
|
||||||
|
"model": "example/model",
|
||||||
|
"num_gpus": 1,
|
||||||
|
"width": 1024,
|
||||||
|
"height": 1024,
|
||||||
|
"num_frames": 81,
|
||||||
|
}
|
||||||
|
command = runner._build_sglang_cmd(
|
||||||
|
case,
|
||||||
|
{
|
||||||
|
"serve_args": (
|
||||||
|
"--warmup-mode server --warmup-resolutions 512x512 "
|
||||||
|
"--warmup-num-frames 25"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
30000,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert command.count("--warmup-resolutions") == 1
|
||||||
|
assert command.count("--warmup-num-frames") == 1
|
||||||
|
assert command[command.index("--warmup-resolutions") + 1] == "512x512"
|
||||||
|
assert command[command.index("--warmup-num-frames") + 1] == "25"
|
||||||
|
|
||||||
|
|
||||||
|
def test_perf_dump_summary_uses_medians():
|
||||||
|
perf_dumps = [
|
||||||
|
{
|
||||||
|
"total_duration_ms": 1000.0,
|
||||||
|
"steps": [
|
||||||
|
{"name": "TextEncodingStage", "duration_ms": 100.0},
|
||||||
|
{"name": "DenoisingStage", "duration_ms": 800.0},
|
||||||
|
],
|
||||||
|
"denoise_steps_ms": [{"duration_ms": 8.0}, {"duration_ms": 10.0}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"total_duration_ms": 3000.0,
|
||||||
|
"steps": [
|
||||||
|
{"name": "TextEncodingStage", "duration_ms": 300.0},
|
||||||
|
{"name": "DenoisingStage", "duration_ms": 2400.0},
|
||||||
|
],
|
||||||
|
"denoise_steps_ms": [{"duration_ms": 30.0}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"total_duration_ms": 1100.0,
|
||||||
|
"steps": [
|
||||||
|
{"name": "TextEncodingStage", "duration_ms": 110.0},
|
||||||
|
{"name": "DenoisingStage", "duration_ms": 880.0},
|
||||||
|
],
|
||||||
|
"denoise_steps_ms": [{"duration_ms": 11.0}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
summary = runner._summarize_perf_dumps(perf_dumps)
|
||||||
|
|
||||||
|
assert summary["server_latency_s"] == 1.1
|
||||||
|
assert summary["server_stage_medians_ms"] == {
|
||||||
|
"DenoisingStage": 880.0,
|
||||||
|
"TextEncodingStage": 110.0,
|
||||||
|
}
|
||||||
|
assert summary["median_denoise_step_ms"] == 10.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_uses_historical_median_and_shows_server_breakdown():
|
||||||
|
current = {
|
||||||
|
"timestamp": "2026-09-04T00:00:00+00:00",
|
||||||
|
"commit_sha": "abcdef123456",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case_id": "example",
|
||||||
|
"framework": "sglang",
|
||||||
|
"model": "example/model",
|
||||||
|
"latency_s": 10.4,
|
||||||
|
"latency_samples_s": [10.3, 10.4, 10.5],
|
||||||
|
"measurement_count": 3,
|
||||||
|
"server_latency_s": 10.0,
|
||||||
|
"server_stage_medians_ms": {
|
||||||
|
"TextEncodingStage": 100.0,
|
||||||
|
"DenoisingStage": 9800.0,
|
||||||
|
"DecodingStage": 100.0,
|
||||||
|
},
|
||||||
|
"median_denoise_step_ms": 196.0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
history = [
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case_id": "example",
|
||||||
|
"framework": "sglang",
|
||||||
|
"latency_s": value,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for value in (10.0, 30.0, 9.8)
|
||||||
|
]
|
||||||
|
|
||||||
|
baseline, count = dashboard._historical_latency_baseline(
|
||||||
|
"example", "sglang", history
|
||||||
|
)
|
||||||
|
markdown, alerts = dashboard.generate_dashboard(current, history)
|
||||||
|
|
||||||
|
assert baseline == 10.0
|
||||||
|
assert count == 3
|
||||||
|
assert alerts == []
|
||||||
|
assert "| 3 | **10.40** |" in markdown
|
||||||
|
assert "## SGLang Server-Side Breakdown" in markdown
|
||||||
|
assert "| model | 10.00 | 0.10 | 9.80 | 0.10 | 196.00 |" in markdown
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import inspect
|
||||||
import os
|
import os
|
||||||
from dataclasses import fields
|
from dataclasses import fields
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.image_api import (
|
|||||||
_runtime_sampling_quality,
|
_runtime_sampling_quality,
|
||||||
_select_image_variant_cloud_url,
|
_select_image_variant_cloud_url,
|
||||||
_select_image_variant_path,
|
_select_image_variant_path,
|
||||||
|
edits,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
|
from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
|
||||||
ImageGenerationsRequest,
|
ImageGenerationsRequest,
|
||||||
@@ -30,6 +32,10 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
|
|||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_edits_declares_perf_dump_path_form_field():
|
||||||
|
assert "perf_dump_path" in inspect.signature(edits).parameters
|
||||||
|
|
||||||
|
|
||||||
def test_url_response_returns_one_item_per_output_path():
|
def test_url_response_returns_one_item_per_output_path():
|
||||||
paths = ["first.png", "second.png"]
|
paths = ["first.png", "second.png"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import inspect
|
||||||
from dataclasses import fields
|
from dataclasses import fields
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -16,10 +17,15 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.realtime_adapter
|
|||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.video_api import (
|
from sglang.multimodal_gen.runtime.entrypoints.openai.video_api import (
|
||||||
_build_video_sampling_params,
|
_build_video_sampling_params,
|
||||||
_video_request_model_kwargs,
|
_video_request_model_kwargs,
|
||||||
|
create_video,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||||
|
|
||||||
|
|
||||||
|
def test_multipart_video_declares_perf_dump_path_form_field():
|
||||||
|
assert "perf_dump_path" in inspect.signature(create_video).parameters
|
||||||
|
|
||||||
|
|
||||||
def test_video_api_forwards_profiling_options():
|
def test_video_api_forwards_profiling_options():
|
||||||
request = VideoGenerationsRequest(
|
request = VideoGenerationsRequest(
|
||||||
prompt="profile this request",
|
prompt="profile this request",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_comment": "Per-model comparison config. Sampling params omitted where model defaults are correct — only override resolution, seed, and params that differ from defaults.",
|
"_comment": "Per-model comparison config. Sampling params omitted where model defaults are correct — only override resolution, seed, and params that differ from defaults.",
|
||||||
|
"measurement_repeats": 3,
|
||||||
"test_image_url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png",
|
"test_image_url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png",
|
||||||
"cases": [
|
"cases": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Usage:
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import statistics
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -26,6 +27,8 @@ CI_DATA_REPO_NAME = "ci-data-diffusion"
|
|||||||
CI_DATA_BRANCH = "main"
|
CI_DATA_BRANCH = "main"
|
||||||
HISTORY_PREFIX = "diffusion-comparisons"
|
HISTORY_PREFIX = "diffusion-comparisons"
|
||||||
MAX_HISTORY_RUNS = 29
|
MAX_HISTORY_RUNS = 29
|
||||||
|
HISTORICAL_BASELINE_RUNS = 5
|
||||||
|
REGRESSION_THRESHOLD = 0.05
|
||||||
|
|
||||||
# Base URL for chart images pushed to sgl-project/ci-data-diffusion
|
# Base URL for chart images pushed to sgl-project/ci-data-diffusion
|
||||||
CHARTS_RAW_BASE_URL = (
|
CHARTS_RAW_BASE_URL = (
|
||||||
@@ -136,6 +139,19 @@ def _short_sha(sha: str) -> str:
|
|||||||
return sha[:7] if sha and sha != "unknown" else "?"
|
return sha[:7] if sha and sha != "unknown" else "?"
|
||||||
|
|
||||||
|
|
||||||
|
def _historical_latency_baseline(
|
||||||
|
cid: str, framework: str, history: list[dict]
|
||||||
|
) -> tuple[float | None, int]:
|
||||||
|
values = []
|
||||||
|
for run in history[:HISTORICAL_BASELINE_RUNS]:
|
||||||
|
latency = _extract_case_results(run).get(cid, {}).get(framework)
|
||||||
|
if latency is not None:
|
||||||
|
values.append(latency)
|
||||||
|
if not values:
|
||||||
|
return None, 0
|
||||||
|
return statistics.median(values), len(values)
|
||||||
|
|
||||||
|
|
||||||
def _assess_risk(
|
def _assess_risk(
|
||||||
cid: str,
|
cid: str,
|
||||||
current_cases: dict[str, dict[str, float | None]],
|
current_cases: dict[str, dict[str, float | None]],
|
||||||
@@ -146,7 +162,7 @@ def _assess_risk(
|
|||||||
|
|
||||||
Rules (checked in order):
|
Rules (checked in order):
|
||||||
- N/A latency → ❌ broken
|
- N/A latency → ❌ broken
|
||||||
- History exists: SGLang latency >5% vs avg of last 3 runs → ⚠️ regression
|
- History exists: SGLang latency >5% vs median of recent runs → ⚠️ regression
|
||||||
- Competitor exists & SGLang slower → 🔴 competitive risk
|
- Competitor exists & SGLang slower → 🔴 competitive risk
|
||||||
- SGLang faster than all competitors by >20% → 🟢 strong advantage
|
- SGLang faster than all competitors by >20% → 🟢 strong advantage
|
||||||
- SGLang faster than all competitors by ≤20% → 🟡 moderate advantage
|
- SGLang faster than all competitors by ≤20% → 🟡 moderate advantage
|
||||||
@@ -158,22 +174,17 @@ def _assess_risk(
|
|||||||
if sg_lat is None:
|
if sg_lat is None:
|
||||||
return "❌", f"{cid}: SGLang latency is N/A (broken)"
|
return "❌", f"{cid}: SGLang latency is N/A (broken)"
|
||||||
|
|
||||||
# Check regression against 3-run historical average
|
baseline, baseline_runs = _historical_latency_baseline(cid, "sglang", history)
|
||||||
if history:
|
if (
|
||||||
hist_lats: list[float] = []
|
baseline is not None
|
||||||
for run in history[:3]:
|
and baseline > 0
|
||||||
run_cases = _extract_case_results(run)
|
and (sg_lat - baseline) / baseline > REGRESSION_THRESHOLD
|
||||||
h_lat = run_cases.get(cid, {}).get("sglang")
|
):
|
||||||
if h_lat is not None:
|
pct = (sg_lat - baseline) / baseline * 100
|
||||||
hist_lats.append(h_lat)
|
|
||||||
if hist_lats:
|
|
||||||
avg_3 = sum(hist_lats) / len(hist_lats)
|
|
||||||
if avg_3 > 0 and (sg_lat - avg_3) / avg_3 > 0.05:
|
|
||||||
pct = (sg_lat - avg_3) / avg_3 * 100
|
|
||||||
return (
|
return (
|
||||||
"⚠️",
|
"⚠️",
|
||||||
f"{cid}: SGLang regression +{pct:.1f}% vs 3-run avg "
|
f"{cid}: SGLang regression +{pct:.1f}% vs {baseline_runs}-run "
|
||||||
f"({sg_lat:.2f}s vs {avg_3:.2f}s)",
|
f"median ({sg_lat:.2f}s vs {baseline:.2f}s)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check competitive risk
|
# Check competitive risk
|
||||||
@@ -229,6 +240,24 @@ def _extract_case_results(run_data: dict) -> dict[str, dict[str, float | None]]:
|
|||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_case_records(run_data: dict) -> dict[str, dict[str, dict]]:
|
||||||
|
"""Extract {case_id: {framework: result record}} from a run."""
|
||||||
|
mapping: dict[str, dict[str, dict]] = {}
|
||||||
|
for result in run_data.get("results", []):
|
||||||
|
mapping.setdefault(result["case_id"], {})[result["framework"]] = result
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_group_seconds(result: dict, suffixes: str | tuple[str, ...]) -> float | None:
|
||||||
|
if isinstance(suffixes, str):
|
||||||
|
suffixes = (suffixes,)
|
||||||
|
stages = result.get("server_stage_medians_ms") or {}
|
||||||
|
values = [value for name, value in stages.items() if name.endswith(suffixes)]
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
return sum(values) / 1000.0
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_filename(name: str) -> str:
|
def _sanitize_filename(name: str) -> str:
|
||||||
"""Sanitize a case ID to be a safe filename."""
|
"""Sanitize a case ID to be a safe filename."""
|
||||||
return name.replace("/", "_").replace(" ", "_").replace(":", "_")
|
return name.replace("/", "_").replace(" ", "_").replace(":", "_")
|
||||||
@@ -257,22 +286,22 @@ def generate_dashboard(
|
|||||||
lines.append(f"*Generated: {_short_date(ts)} | Commit: `{_short_sha(sha)}`*\n")
|
lines.append(f"*Generated: {_short_date(ts)} | Commit: `{_short_sha(sha)}`*\n")
|
||||||
|
|
||||||
current_cases = _extract_case_results(current)
|
current_cases = _extract_case_results(current)
|
||||||
|
current_records = _extract_case_records(current)
|
||||||
case_ids = list(current_cases.keys())
|
case_ids = list(current_cases.keys())
|
||||||
|
|
||||||
# ---- Regression detection ----
|
# ---- Regression detection ----
|
||||||
REGRESSION_THRESHOLD = 0.05 # 5%
|
|
||||||
regressions: list[str] = []
|
regressions: list[str] = []
|
||||||
if history:
|
if history:
|
||||||
prev_cases = _extract_case_results(history[0])
|
|
||||||
for cid in case_ids:
|
for cid in case_ids:
|
||||||
for fw in ("sglang", "vllm-omni"):
|
for fw in ("sglang", "vllm-omni"):
|
||||||
cur = current_cases.get(cid, {}).get(fw)
|
cur = current_cases.get(cid, {}).get(fw)
|
||||||
prev = prev_cases.get(cid, {}).get(fw)
|
baseline, baseline_runs = _historical_latency_baseline(cid, fw, history)
|
||||||
if cur and prev and prev > 0:
|
if cur is not None and baseline is not None and baseline > 0:
|
||||||
pct = (cur - prev) / prev
|
pct = (cur - baseline) / baseline
|
||||||
if pct > REGRESSION_THRESHOLD:
|
if pct > REGRESSION_THRESHOLD:
|
||||||
regressions.append(
|
regressions.append(
|
||||||
f"**{cid}** ({fw}): {prev:.2f}s -> {cur:.2f}s "
|
f"**{cid}** ({fw}): {cur:.2f}s vs "
|
||||||
|
f"{baseline_runs}-run median {baseline:.2f}s "
|
||||||
f"(+{pct * 100:.1f}%)"
|
f"(+{pct * 100:.1f}%)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,10 +334,10 @@ def generate_dashboard(
|
|||||||
risk_map[cid] = _assess_risk(cid, current_cases, history, other_frameworks)
|
risk_map[cid] = _assess_risk(cid, current_cases, history, other_frameworks)
|
||||||
|
|
||||||
# Dynamic header
|
# Dynamic header
|
||||||
header = "| Model | Risk |"
|
header = "| Model | Risk | Samples |"
|
||||||
sep = "|-------|------|"
|
sep = "|-------|------|---------|"
|
||||||
for fw in all_frameworks:
|
for fw in all_frameworks:
|
||||||
header += f" {fw} (s) |"
|
header += f" {fw} median (s) |"
|
||||||
sep += "---------|"
|
sep += "---------|"
|
||||||
for ofw in other_frameworks:
|
for ofw in other_frameworks:
|
||||||
header += f" vs {ofw} |"
|
header += f" vs {ofw} |"
|
||||||
@@ -326,9 +355,15 @@ def generate_dashboard(
|
|||||||
|
|
||||||
case_fws = current_cases.get(cid, {})
|
case_fws = current_cases.get(cid, {})
|
||||||
sg_lat = case_fws.get("sglang")
|
sg_lat = case_fws.get("sglang")
|
||||||
|
sg_record = current_records.get(cid, {}).get("sglang", {})
|
||||||
|
sample_count = sg_record.get("measurement_count")
|
||||||
|
if not sample_count and sg_lat is not None:
|
||||||
|
sample_count = 1
|
||||||
|
|
||||||
risk_emoji, _ = risk_map.get(cid, ("✅", ""))
|
risk_emoji, _ = risk_map.get(cid, ("✅", ""))
|
||||||
row = f"| {r['model'].split('/')[-1]} | {risk_emoji} |"
|
row = (
|
||||||
|
f"| {r['model'].split('/')[-1]} | {risk_emoji} | {sample_count or 'N/A'} |"
|
||||||
|
)
|
||||||
# Latency columns -- bold the fastest
|
# Latency columns -- bold the fastest
|
||||||
lats = {fw: case_fws.get(fw) for fw in all_frameworks}
|
lats = {fw: case_fws.get(fw) for fw in all_frameworks}
|
||||||
valid_lats = [v for v in lats.values() if v is not None]
|
valid_lats = [v for v in lats.values() if v is not None]
|
||||||
@@ -344,6 +379,38 @@ def generate_dashboard(
|
|||||||
row += f" {_fmt_speedup(sg_lat, case_fws.get(ofw))} |"
|
row += f" {_fmt_speedup(sg_lat, case_fws.get(ofw))} |"
|
||||||
lines.append(row)
|
lines.append(row)
|
||||||
|
|
||||||
|
server_records = [
|
||||||
|
current_records.get(cid, {}).get("sglang", {}) for cid in case_ids
|
||||||
|
]
|
||||||
|
if any(record.get("server_latency_s") is not None for record in server_records):
|
||||||
|
lines.append("\n## SGLang Server-Side Breakdown\n")
|
||||||
|
lines.append(
|
||||||
|
"| Model | Server total (s) | Text encode (s) | Denoise (s) | "
|
||||||
|
"Decode (s) | Median denoise step (ms) |"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
"|-------|------------------|-----------------|--------------|"
|
||||||
|
"------------|---------------------------|"
|
||||||
|
)
|
||||||
|
for record in server_records:
|
||||||
|
if record.get("server_latency_s") is None:
|
||||||
|
continue
|
||||||
|
model = record["model"].split("/")[-1]
|
||||||
|
text_encode = _stage_group_seconds(
|
||||||
|
record, ("TextEncodingStage", "TokenizationStage")
|
||||||
|
)
|
||||||
|
denoise = _stage_group_seconds(record, "DenoisingStage")
|
||||||
|
decode = _stage_group_seconds(record, "DecodingStage")
|
||||||
|
denoise_step = record.get("median_denoise_step_ms")
|
||||||
|
denoise_step_text = (
|
||||||
|
f"{denoise_step:.2f}" if denoise_step is not None else "N/A"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"| {model} | {_fmt_latency(record['server_latency_s'])} | "
|
||||||
|
f"{_fmt_latency(text_encode)} | {_fmt_latency(denoise)} | "
|
||||||
|
f"{_fmt_latency(decode)} | {denoise_step_text} |"
|
||||||
|
)
|
||||||
|
|
||||||
# ---- Section 2: Speedup-over-time vs. other frameworks (rendered only when present) ----
|
# ---- Section 2: Speedup-over-time vs. other frameworks (rendered only when present) ----
|
||||||
if history and other_frameworks:
|
if history and other_frameworks:
|
||||||
lines.append("\n## SGLang vs vLLM-Omni Speedup Over Time\n")
|
lines.append("\n## SGLang vs vLLM-Omni Speedup Over Time\n")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Diffusion serving benchmark for SGLang-Diffusion nightly CI.
|
"""Diffusion serving benchmark for SGLang-Diffusion nightly CI.
|
||||||
|
|
||||||
Launches an SGLang-Diffusion server for each test case, sends a single
|
Launches an SGLang-Diffusion server for each test case, sends repeated
|
||||||
request, measures end-to-end latency, and writes comparison-results.json.
|
requests, measures median end-to-end latency, and writes comparison-results.json.
|
||||||
The runner still supports extra frameworks via --frameworks, but the nightly
|
The runner still supports extra frameworks via --frameworks, but the nightly
|
||||||
config tracks SGLang-Diffusion only.
|
config tracks SGLang-Diffusion only.
|
||||||
|
|
||||||
@@ -24,8 +24,10 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
|
import statistics
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -84,12 +86,29 @@ def _build_sglang_cmd(case: dict, fw_cfg: dict, port: int) -> list[str]:
|
|||||||
]
|
]
|
||||||
if case["num_gpus"] > 1:
|
if case["num_gpus"] > 1:
|
||||||
cmd += ["--num-gpus", str(case["num_gpus"])]
|
cmd += ["--num-gpus", str(case["num_gpus"])]
|
||||||
if fw_cfg.get("serve_args", "").strip():
|
serve_args = shlex.split(fw_cfg.get("serve_args", ""))
|
||||||
cmd += fw_cfg["serve_args"].strip().split()
|
cmd += serve_args
|
||||||
# No explicit --warmup-resolutions: server-based warmup now defaults to the
|
|
||||||
# model's sampling-default resolution (see warmup_request_builder), which
|
def has_option(name: str) -> bool:
|
||||||
# already matches these single-resolution cases — the default warmup is
|
return any(arg == name or arg.startswith(f"{name}=") for arg in serve_args)
|
||||||
# sufficient, so we don't pin a resolution here.
|
|
||||||
|
server_warmup = any(
|
||||||
|
arg == "--warmup-mode=server"
|
||||||
|
or (
|
||||||
|
arg == "--warmup-mode"
|
||||||
|
and index + 1 < len(serve_args)
|
||||||
|
and serve_args[index + 1] == "server"
|
||||||
|
)
|
||||||
|
for index, arg in enumerate(serve_args)
|
||||||
|
)
|
||||||
|
if server_warmup and not has_option("--warmup-resolutions"):
|
||||||
|
cmd += ["--warmup-resolutions", f"{case['width']}x{case['height']}"]
|
||||||
|
if (
|
||||||
|
server_warmup
|
||||||
|
and case.get("num_frames") is not None
|
||||||
|
and not has_option("--warmup-num-frames")
|
||||||
|
):
|
||||||
|
cmd += ["--warmup-num-frames", str(case["num_frames"])]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
@@ -375,8 +394,8 @@ def _build_sglang_payload(case: dict) -> dict:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def _read_perf_dump(perf_dump_path: str, timeout: float = 10.0) -> float | None:
|
def _read_perf_dump(perf_dump_path: str, timeout: float = 10.0) -> dict | None:
|
||||||
"""Read total_duration_ms from a perf dump JSON written by the server.
|
"""Read a perf dump JSON written by the server.
|
||||||
|
|
||||||
The server writes the file asynchronously after the HTTP response,
|
The server writes the file asynchronously after the HTTP response,
|
||||||
so we poll briefly.
|
so we poll briefly.
|
||||||
@@ -386,9 +405,8 @@ def _read_perf_dump(perf_dump_path: str, timeout: float = 10.0) -> float | None:
|
|||||||
try:
|
try:
|
||||||
with open(perf_dump_path) as f:
|
with open(perf_dump_path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
total_ms = data.get("total_duration_ms")
|
if data.get("total_duration_ms") is not None:
|
||||||
if total_ms is not None:
|
return data
|
||||||
return total_ms / 1000.0
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
pass
|
pass
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
@@ -415,16 +433,6 @@ def send_image_request_sglang(
|
|||||||
if "data" not in data or len(data["data"]) == 0:
|
if "data" not in data or len(data["data"]) == 0:
|
||||||
raise RuntimeError(f"Image request returned no data: {data}")
|
raise RuntimeError(f"Image request returned no data: {data}")
|
||||||
|
|
||||||
# Report client-side e2e latency to match vllm-omni / lightx2v (fair
|
|
||||||
# cross-framework comparison); server-side perf_dump is diagnostic only.
|
|
||||||
if perf_dump_path:
|
|
||||||
server_latency = _read_perf_dump(perf_dump_path)
|
|
||||||
if server_latency is not None:
|
|
||||||
print(
|
|
||||||
f" Image generated in {client_latency:.2f}s (client e2e; "
|
|
||||||
f"server-side {server_latency:.2f}s, diagnostic)"
|
|
||||||
)
|
|
||||||
return client_latency
|
|
||||||
print(f" Image generated in {client_latency:.2f}s")
|
print(f" Image generated in {client_latency:.2f}s")
|
||||||
return client_latency
|
return client_latency
|
||||||
|
|
||||||
@@ -468,16 +476,6 @@ def send_video_request_sglang(
|
|||||||
|
|
||||||
client_latency = time.time() - start
|
client_latency = time.time() - start
|
||||||
|
|
||||||
# Report client-side e2e latency to match vllm-omni / lightx2v (fair
|
|
||||||
# cross-framework comparison); server-side perf_dump is diagnostic only.
|
|
||||||
if perf_dump_path:
|
|
||||||
server_latency = _read_perf_dump(perf_dump_path)
|
|
||||||
if server_latency is not None:
|
|
||||||
print(
|
|
||||||
f" Video generated in {client_latency:.2f}s (client e2e; "
|
|
||||||
f"server-side {server_latency:.2f}s, diagnostic)"
|
|
||||||
)
|
|
||||||
return client_latency
|
|
||||||
print(f" Video generated in {client_latency:.2f}s")
|
print(f" Video generated in {client_latency:.2f}s")
|
||||||
return client_latency
|
return client_latency
|
||||||
|
|
||||||
@@ -556,16 +554,6 @@ def send_image_conditioned_request_sglang(
|
|||||||
|
|
||||||
client_latency = time.time() - start
|
client_latency = time.time() - start
|
||||||
|
|
||||||
# Report client-side e2e latency to match vllm-omni / lightx2v (fair
|
|
||||||
# cross-framework comparison); server-side perf_dump is diagnostic only.
|
|
||||||
if perf_dump_path:
|
|
||||||
server_latency = _read_perf_dump(perf_dump_path)
|
|
||||||
if server_latency is not None:
|
|
||||||
print(
|
|
||||||
f" Generated in {client_latency:.2f}s (client e2e; "
|
|
||||||
f"server-side {server_latency:.2f}s, diagnostic)"
|
|
||||||
)
|
|
||||||
return client_latency
|
|
||||||
print(f" Generated in {client_latency:.2f}s (sglang, image-conditioned)")
|
print(f" Generated in {client_latency:.2f}s (sglang, image-conditioned)")
|
||||||
return client_latency
|
return client_latency
|
||||||
|
|
||||||
@@ -730,6 +718,39 @@ def send_request(
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_perf_dumps(perf_dumps: list[dict]) -> dict:
|
||||||
|
server_latency_samples_s = [
|
||||||
|
round(dump["total_duration_ms"] / 1000.0, 3) for dump in perf_dumps
|
||||||
|
]
|
||||||
|
stage_samples: dict[str, list[float]] = {}
|
||||||
|
denoise_step_samples: list[float] = []
|
||||||
|
for dump in perf_dumps:
|
||||||
|
for stage in dump.get("steps", []):
|
||||||
|
name = stage.get("name")
|
||||||
|
duration_ms = stage.get("duration_ms")
|
||||||
|
if name and duration_ms is not None:
|
||||||
|
stage_samples.setdefault(name, []).append(float(duration_ms))
|
||||||
|
denoise_step_samples.extend(
|
||||||
|
float(step["duration_ms"])
|
||||||
|
for step in dump.get("denoise_steps_ms", [])
|
||||||
|
if step.get("duration_ms") is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"server_latency_samples_s": server_latency_samples_s,
|
||||||
|
"server_latency_s": round(statistics.median(server_latency_samples_s), 3),
|
||||||
|
"server_stage_medians_ms": {
|
||||||
|
name: round(statistics.median(values), 3)
|
||||||
|
for name, values in sorted(stage_samples.items())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if denoise_step_samples:
|
||||||
|
summary["median_denoise_step_ms"] = round(
|
||||||
|
statistics.median(denoise_step_samples), 3
|
||||||
|
)
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def run_single(
|
def run_single(
|
||||||
case: dict,
|
case: dict,
|
||||||
framework: str,
|
framework: str,
|
||||||
@@ -737,6 +758,7 @@ def run_single(
|
|||||||
port: int,
|
port: int,
|
||||||
log_dir: Path,
|
log_dir: Path,
|
||||||
config: dict | None = None,
|
config: dict | None = None,
|
||||||
|
measurement_repeats: int = 1,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Run a single (case, framework) combination. Returns result dict."""
|
"""Run a single (case, framework) combination. Returns result dict."""
|
||||||
result = {
|
result = {
|
||||||
@@ -745,6 +767,8 @@ def run_single(
|
|||||||
"model": case["model"],
|
"model": case["model"],
|
||||||
"task": case["task"],
|
"task": case["task"],
|
||||||
"latency_s": None,
|
"latency_s": None,
|
||||||
|
"latency_samples_s": [],
|
||||||
|
"measurement_count": 0,
|
||||||
"error": None,
|
"error": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,11 +778,6 @@ def run_single(
|
|||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(fw_cfg.get("extra_env", {}))
|
env.update(fw_cfg.get("extra_env", {}))
|
||||||
|
|
||||||
# perf_dump_path for SGLang server-side timing (passed in request, zero overhead when None)
|
|
||||||
perf_dump_path = None
|
|
||||||
if framework == "sglang":
|
|
||||||
perf_dump_path = os.path.join(str(log_dir), f"perf_{case['id']}_measured.json")
|
|
||||||
|
|
||||||
log_file = log_dir / f"{case['id']}_{framework}.log"
|
log_file = log_dir / f"{case['id']}_{framework}.log"
|
||||||
log_fh = open(log_file, "w", encoding="utf-8", buffering=1)
|
log_fh = open(log_file, "w", encoding="utf-8", buffering=1)
|
||||||
log_thread = None
|
log_thread = None
|
||||||
@@ -811,26 +830,54 @@ def run_single(
|
|||||||
base_url = f"http://{DEFAULT_HOST}:{port}"
|
base_url = f"http://{DEFAULT_HOST}:{port}"
|
||||||
wait_for_health(base_url, framework)
|
wait_for_health(base_url, framework)
|
||||||
|
|
||||||
# No client-side warmup: each framework relies on its own server-side
|
# SGLang server warmup uses the measured shape added by
|
||||||
# warmup before traffic. sglang's serve_args pass --warmup-mode server,
|
# _build_sglang_cmd. The repeated requests below absorb any remaining
|
||||||
# which primes kernels with a synthetic request at startup, before the
|
# request-path cold effects, including image-conditioned preprocessing.
|
||||||
# health check passes. This goes through the internal
|
|
||||||
# warmup path that bypasses sampling-param preset validation (e.g.
|
|
||||||
# Ideogram-4's preset-locked num_inference_steps), so no per-case warmup
|
|
||||||
# special-casing is needed here.
|
|
||||||
# NOTE: vllm-omni / lightx2v configure no server-side warmup; if
|
# NOTE: vllm-omni / lightx2v configure no server-side warmup; if
|
||||||
# cross-framework comparison is restored, they must add their own warmup
|
# cross-framework comparison is restored, they must add their own warmup
|
||||||
# to stay on equal footing — otherwise their measured request pays the
|
# to stay on equal footing — otherwise their measured request pays the
|
||||||
# full cold-start.
|
# full cold-start.
|
||||||
|
|
||||||
# Measured request — pass perf_dump_path for SGLang server-side timing
|
latency_samples: list[float] = []
|
||||||
if perf_dump_path and os.path.exists(perf_dump_path):
|
perf_dumps: list[dict] = []
|
||||||
|
for sample_index in range(measurement_repeats):
|
||||||
|
perf_dump_path = None
|
||||||
|
if framework == "sglang":
|
||||||
|
sample_suffix = "" if sample_index == 0 else f"_{sample_index + 1}"
|
||||||
|
perf_dump_path = str(
|
||||||
|
(
|
||||||
|
log_dir / f"perf_{case['id']}_measured{sample_suffix}.json"
|
||||||
|
).resolve()
|
||||||
|
)
|
||||||
|
if os.path.exists(perf_dump_path):
|
||||||
os.remove(perf_dump_path)
|
os.remove(perf_dump_path)
|
||||||
print(" Sending measured request...")
|
|
||||||
|
print(
|
||||||
|
f" Sending measured request {sample_index + 1}/"
|
||||||
|
f"{measurement_repeats}..."
|
||||||
|
)
|
||||||
latency = send_request(
|
latency = send_request(
|
||||||
base_url, case, framework, config, perf_dump_path=perf_dump_path
|
base_url, case, framework, config, perf_dump_path=perf_dump_path
|
||||||
)
|
)
|
||||||
result["latency_s"] = round(latency, 3)
|
latency_samples.append(round(latency, 3))
|
||||||
|
|
||||||
|
if perf_dump_path:
|
||||||
|
perf_dump = _read_perf_dump(perf_dump_path)
|
||||||
|
if perf_dump is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Server did not write performance data to {perf_dump_path}"
|
||||||
|
)
|
||||||
|
perf_dumps.append(perf_dump)
|
||||||
|
print(
|
||||||
|
" Server-side latency: "
|
||||||
|
f"{perf_dump['total_duration_ms'] / 1000.0:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
result["latency_samples_s"] = latency_samples
|
||||||
|
result["measurement_count"] = len(latency_samples)
|
||||||
|
result["latency_s"] = round(statistics.median(latency_samples), 3)
|
||||||
|
if perf_dumps:
|
||||||
|
result.update(_summarize_perf_dumps(perf_dumps))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result["error"] = server_error.get("message", str(e))
|
result["error"] = server_error.get("message", str(e))
|
||||||
@@ -889,6 +936,7 @@ def run_comparison(
|
|||||||
port: int = DEFAULT_PORT,
|
port: int = DEFAULT_PORT,
|
||||||
output: str = "comparison-results.json",
|
output: str = "comparison-results.json",
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
|
measurement_repeats: int | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Run all comparison cases, grouped by framework to minimize installs.
|
"""Run all comparison cases, grouped by framework to minimize installs.
|
||||||
|
|
||||||
@@ -898,6 +946,13 @@ def run_comparison(
|
|||||||
timestamp = datetime.now(timezone.utc).isoformat()
|
timestamp = datetime.now(timezone.utc).isoformat()
|
||||||
commit_sha = _get_checkout_commit_sha()
|
commit_sha = _get_checkout_commit_sha()
|
||||||
run_id = os.environ.get("GITHUB_RUN_ID", "local")
|
run_id = os.environ.get("GITHUB_RUN_ID", "local")
|
||||||
|
repeats = (
|
||||||
|
measurement_repeats
|
||||||
|
if measurement_repeats is not None
|
||||||
|
else int(config.get("measurement_repeats", 1))
|
||||||
|
)
|
||||||
|
if repeats <= 0:
|
||||||
|
raise ValueError("measurement_repeats must be a positive integer")
|
||||||
|
|
||||||
log_dir = Path("comparison-logs")
|
log_dir = Path("comparison-logs")
|
||||||
log_dir.mkdir(exist_ok=True)
|
log_dir.mkdir(exist_ok=True)
|
||||||
@@ -962,7 +1017,15 @@ def run_comparison(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = run_single(case, fw_name, fw_cfg, port, log_dir, config)
|
result = run_single(
|
||||||
|
case,
|
||||||
|
fw_name,
|
||||||
|
fw_cfg,
|
||||||
|
port,
|
||||||
|
log_dir,
|
||||||
|
config,
|
||||||
|
measurement_repeats=repeats,
|
||||||
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
# Wait for GPU memory to clear
|
# Wait for GPU memory to clear
|
||||||
@@ -973,6 +1036,7 @@ def run_comparison(
|
|||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
"commit_sha": commit_sha,
|
"commit_sha": commit_sha,
|
||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
|
"measurement_repeats": repeats,
|
||||||
"results": results,
|
"results": results,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +1050,11 @@ def run_comparison(
|
|||||||
print("SUMMARY")
|
print("SUMMARY")
|
||||||
print(f"{'=' * 60}")
|
print(f"{'=' * 60}")
|
||||||
for r in results:
|
for r in results:
|
||||||
lat = f"{r['latency_s']:.2f}s" if r["latency_s"] else r.get("error", "N/A")
|
lat = (
|
||||||
|
f"{r['latency_s']:.2f}s median (n={r.get('measurement_count', 1)})"
|
||||||
|
if r["latency_s"]
|
||||||
|
else r.get("error", "N/A")
|
||||||
|
)
|
||||||
print(f" {r['case_id']:30s} | {r['framework']:12s} | {lat}")
|
print(f" {r['case_id']:30s} | {r['framework']:12s} | {lat}")
|
||||||
|
|
||||||
return output_data
|
return output_data
|
||||||
@@ -1034,6 +1102,12 @@ def main():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Parse config and print commands without launching servers",
|
help="Parse config and print commands without launching servers",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--measurement-repeats",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Measured requests per case (default: value from config)",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -1049,6 +1123,7 @@ def main():
|
|||||||
port=args.port,
|
port=args.port,
|
||||||
output=args.output,
|
output=args.output,
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
|
measurement_repeats=args.measurement_repeats,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Exit with non-zero if any case had an error
|
# Exit with non-zero if any case had an error
|
||||||
|
|||||||
Reference in New Issue
Block a user