[diffusion] CI: add per-case timing tolerance for host-I/O-bound perf guards (#38457)

Co-authored-by: Mick Qian <mickqian@radixark.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mick
2026-09-08 18:40:17 +08:00
committed by GitHub
co-authored by Mick Qian Claude Opus 5
parent e634ba78a4
commit 88a9bfd1ff
5 changed files with 98 additions and 10 deletions
@@ -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
}
}
}
@@ -700,6 +700,13 @@ class DiffusionServerBase:
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"]:
@@ -726,6 +726,18 @@ class PerformanceValidator:
) -> 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"
@@ -733,7 +745,7 @@ class PerformanceValidator:
"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:
@@ -744,13 +756,13 @@ class PerformanceValidator:
"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:
@@ -766,7 +778,7 @@ class PerformanceValidator:
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
@@ -775,7 +787,7 @@ class PerformanceValidator:
f"Denoise Step {idx}",
actual,
expected,
self.tolerances.denoise_step,
self._timing_tol(self.tolerances.denoise_step),
)
def _validate_stages(self, summary: PerformanceSummary) -> None:
@@ -787,7 +799,7 @@ class PerformanceValidator:
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
@@ -822,7 +834,7 @@ class VideoPerformanceValidator(PerformanceValidator):
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
@@ -833,7 +845,7 @@ class VideoPerformanceValidator(PerformanceValidator):
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,
)
@@ -863,7 +875,7 @@ class VideoPerformanceValidator(PerformanceValidator):
"Average Frame Time",
summary.avg_frame_time_ms,
expected_frame_time,
self.tolerances.denoise_stage,
self._timing_tol(self.tolerances.denoise_stage),
)
@@ -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:
@@ -148,6 +153,7 @@ class ScenarioConfig:
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"),
)
@@ -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)