Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@
"runtime_peak_allocated_mb": 11483.0,
"load_peak_host_anon_mb": 32768.0,
"runtime_peak_host_anon_mb": 32768.0,
"estimated_full_test_time_s": 283.0
"estimated_full_test_time_s": 283.0,
"timing_tolerance": 0.9
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,13 @@ def _dump_baseline_for_testcase(
if measured_full_time is not None:
baseline["estimated_full_test_time_s"] = round(measured_full_time, 1)

# a per-case timing tolerance is a deliberate property of the case, not a
# measurement: carry it into the suggested entry so a baseline refresh
# cannot silently drop it
existing = BASELINE_CONFIG.scenarios.get(case.id)
if existing is not None and existing.timing_tolerance is not None:
baseline["timing_tolerance"] = existing.timing_tolerance

# Video-specific metrics
if case.server_args.modality == "video":
if "per_frame_generation" not in baseline["stages_ms"]:
Expand Down
30 changes: 21 additions & 9 deletions python/sglang/multimodal_gen/test/server/test_server_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,14 +726,26 @@ def collect_metrics(
) -> PerformanceSummary:
return PerformanceSummary.from_req_perf_record(perf_record, self.step_fractions)

def _timing_tol(self, profile_tolerance: float) -> float:
"""Tolerance for a wall-clock check, honoring a per-case override.

A case whose runtime is dominated by shared-runner host I/O cannot be
guarded at the profile tolerance; ``timing_tolerance`` in its baseline
entry widens only the wall-clock checks, never the memory ones.
"""
override = self.scenario.timing_tolerance
if override is None:
return profile_tolerance
return max(profile_tolerance, override)

def _validate_e2e(self, summary: PerformanceSummary) -> None:
"""Validate end-to-end performance."""
assert summary.e2e_ms > 0, "E2E duration missing"
self._assert_le(
"E2E Latency",
summary.e2e_ms,
self.scenario.expected_e2e_ms,
self.tolerances.e2e,
self._timing_tol(self.tolerances.e2e),
)

def _validate_denoise_agg(self, summary: PerformanceSummary) -> None:
Expand All @@ -744,13 +756,13 @@ def _validate_denoise_agg(self, summary: PerformanceSummary) -> None:
"Average Denoise Step",
summary.avg_denoise_ms,
self.scenario.expected_avg_denoise_ms,
self.tolerances.denoise_agg,
self._timing_tol(self.tolerances.denoise_agg),
)
self._assert_le(
"Median Denoise Step",
summary.median_denoise_ms,
self.scenario.expected_median_denoise_ms,
self.tolerances.denoise_agg,
self._timing_tol(self.tolerances.denoise_agg),
)

def _validate_denoise_steps(self, summary: PerformanceSummary) -> None:
Expand All @@ -766,7 +778,7 @@ def _validate_denoise_steps(self, summary: PerformanceSummary) -> None:
f"Denoise Step {idx}",
actual,
expected,
FIRST_DENOISE_STEP_TOLERANCE,
self._timing_tol(FIRST_DENOISE_STEP_TOLERANCE),
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
)
continue
Expand All @@ -775,7 +787,7 @@ def _validate_denoise_steps(self, summary: PerformanceSummary) -> None:
f"Denoise Step {idx}",
actual,
expected,
self.tolerances.denoise_step,
self._timing_tol(self.tolerances.denoise_step),
)

def _validate_stages(self, summary: PerformanceSummary) -> None:
Expand All @@ -787,7 +799,7 @@ def _validate_stages(self, summary: PerformanceSummary) -> None:
continue
actual = summary.stage_metrics.get(stage)
assert actual is not None, f"Stage {stage} timing missing"
tolerance = (
tolerance = self._timing_tol(
self.tolerances.denoise_stage
if stage == "DenoisingStage"
else self.tolerances.non_denoise_stage
Expand Down Expand Up @@ -822,7 +834,7 @@ def _validate_denoise_steps(self, summary: PerformanceSummary) -> None:
f"Denoise Step {idx}",
actual,
expected,
FIRST_DENOISE_STEP_TOLERANCE,
self._timing_tol(FIRST_DENOISE_STEP_TOLERANCE),
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
)
continue
Expand All @@ -833,7 +845,7 @@ def _validate_denoise_steps(self, summary: PerformanceSummary) -> None:
f"Denoise Step {idx}",
actual,
expected,
self.tolerances.denoise_step,
self._timing_tol(self.tolerances.denoise_step),
min_abs_tolerance=VIDEO_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
)

Expand Down Expand Up @@ -863,7 +875,7 @@ def _validate_frame_rate(self, summary: PerformanceSummary) -> None:
"Average Frame Time",
summary.avg_frame_time_ms,
expected_frame_time,
self.tolerances.denoise_stage,
self._timing_tol(self.tolerances.denoise_stage),
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ class ScenarioConfig:
# Anonymous-host budget caps; None skips the check (older baselines).
load_peak_host_anon_mb: float | None = None
runtime_peak_host_anon_mb: float | None = None
# Per-case override for the wall-clock tolerances (e2e, denoise and stage
# timings) when a case's runtime is dominated by shared-runner host I/O
# rather than by the code under test. Memory guards keep the profile
# tolerance -- they are what such a case actually protects.
timing_tolerance: float | None = None

@classmethod
def from_dict(cls, cfg: dict[str, Any]) -> ScenarioConfig:
Expand All @@ -148,6 +153,7 @@ def optional_float(name: str) -> float | None:
runtime_peak_allocated_mb=optional_float("runtime_peak_allocated_mb"),
load_peak_host_anon_mb=optional_float("load_peak_host_anon_mb"),
runtime_peak_host_anon_mb=optional_float("runtime_peak_host_anon_mb"),
timing_tolerance=optional_float("timing_tolerance"),
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,65 @@ def test_worker_keeps_the_pool_when_no_probe_ran(monkeypatch):
worker._release_warmup_pool(_warmup_req())
worker._release_warmup_pool(SimpleNamespace(is_warmup=False, extra={}))
assert calls == []


def _timing_validator(timing_tolerance: float | None) -> PerformanceValidator:
return PerformanceValidator(
scenario=ScenarioConfig(
stages_ms={"DenoisingStage": 1000.0},
denoise_step_ms={1: 100.0},
expected_e2e_ms=1000.0,
expected_avg_denoise_ms=100.0,
expected_median_denoise_ms=100.0,
timing_tolerance=timing_tolerance,
),
tolerances=ToleranceConfig(0.25, 0.25, 0.25, 0.3, 0.2),
step_fractions=(),
)


def _validate_scaled(validator: PerformanceValidator, scale: float) -> None:
summary = PerformanceSummary(
1000.0 * scale,
100.0 * scale,
100.0 * scale,
{"DenoisingStage": 1000.0 * scale},
[],
{1: 100.0 * scale},
{1: 100.0 * scale},
)
validator.collect_metrics = lambda _record: summary
validator.validate(None)


def test_timing_tolerance_override_widens_wall_clock_checks_only():
with patch.object(current_platform, "is_hip", return_value=False):
# 1.7x the baseline fails at the profile's 25% e2e tolerance
with pytest.raises(AssertionError, match="E2E Latency"):
_validate_scaled(_timing_validator(None), 1.7)

# the same run passes for a case that declares a 90% timing tolerance
_validate_scaled(_timing_validator(0.9), 1.7)

# the override is a ceiling, not a blank cheque
with pytest.raises(AssertionError, match="E2E Latency"):
_validate_scaled(_timing_validator(0.9), 2.5)


def test_timing_tolerance_override_leaves_memory_guards_alone():
validator = _timing_validator(0.9)
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="Runtime Peak VRAM"):
validator.validate_peak_vram(regression, 10_000.0, 10_000.0)
Loading