[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:
co-authored by
Mick Qian
Claude Opus 5
parent
e634ba78a4
commit
88a9bfd1ff
@@ -223,7 +223,8 @@
|
|||||||
"runtime_peak_allocated_mb": 11483.0,
|
"runtime_peak_allocated_mb": 11483.0,
|
||||||
"load_peak_host_anon_mb": 32768.0,
|
"load_peak_host_anon_mb": 32768.0,
|
||||||
"runtime_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:
|
if measured_full_time is not None:
|
||||||
baseline["estimated_full_test_time_s"] = round(measured_full_time, 1)
|
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
|
# Video-specific metrics
|
||||||
if case.server_args.modality == "video":
|
if case.server_args.modality == "video":
|
||||||
if "per_frame_generation" not in baseline["stages_ms"]:
|
if "per_frame_generation" not in baseline["stages_ms"]:
|
||||||
|
|||||||
@@ -726,6 +726,18 @@ class PerformanceValidator:
|
|||||||
) -> PerformanceSummary:
|
) -> PerformanceSummary:
|
||||||
return PerformanceSummary.from_req_perf_record(perf_record, self.step_fractions)
|
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:
|
def _validate_e2e(self, summary: PerformanceSummary) -> None:
|
||||||
"""Validate end-to-end performance."""
|
"""Validate end-to-end performance."""
|
||||||
assert summary.e2e_ms > 0, "E2E duration missing"
|
assert summary.e2e_ms > 0, "E2E duration missing"
|
||||||
@@ -733,7 +745,7 @@ class PerformanceValidator:
|
|||||||
"E2E Latency",
|
"E2E Latency",
|
||||||
summary.e2e_ms,
|
summary.e2e_ms,
|
||||||
self.scenario.expected_e2e_ms,
|
self.scenario.expected_e2e_ms,
|
||||||
self.tolerances.e2e,
|
self._timing_tol(self.tolerances.e2e),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_denoise_agg(self, summary: PerformanceSummary) -> None:
|
def _validate_denoise_agg(self, summary: PerformanceSummary) -> None:
|
||||||
@@ -744,13 +756,13 @@ class PerformanceValidator:
|
|||||||
"Average Denoise Step",
|
"Average Denoise Step",
|
||||||
summary.avg_denoise_ms,
|
summary.avg_denoise_ms,
|
||||||
self.scenario.expected_avg_denoise_ms,
|
self.scenario.expected_avg_denoise_ms,
|
||||||
self.tolerances.denoise_agg,
|
self._timing_tol(self.tolerances.denoise_agg),
|
||||||
)
|
)
|
||||||
self._assert_le(
|
self._assert_le(
|
||||||
"Median Denoise Step",
|
"Median Denoise Step",
|
||||||
summary.median_denoise_ms,
|
summary.median_denoise_ms,
|
||||||
self.scenario.expected_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:
|
def _validate_denoise_steps(self, summary: PerformanceSummary) -> None:
|
||||||
@@ -766,7 +778,7 @@ class PerformanceValidator:
|
|||||||
f"Denoise Step {idx}",
|
f"Denoise Step {idx}",
|
||||||
actual,
|
actual,
|
||||||
expected,
|
expected,
|
||||||
FIRST_DENOISE_STEP_TOLERANCE,
|
self._timing_tol(FIRST_DENOISE_STEP_TOLERANCE),
|
||||||
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
|
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -775,7 +787,7 @@ class PerformanceValidator:
|
|||||||
f"Denoise Step {idx}",
|
f"Denoise Step {idx}",
|
||||||
actual,
|
actual,
|
||||||
expected,
|
expected,
|
||||||
self.tolerances.denoise_step,
|
self._timing_tol(self.tolerances.denoise_step),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_stages(self, summary: PerformanceSummary) -> None:
|
def _validate_stages(self, summary: PerformanceSummary) -> None:
|
||||||
@@ -787,7 +799,7 @@ class PerformanceValidator:
|
|||||||
continue
|
continue
|
||||||
actual = summary.stage_metrics.get(stage)
|
actual = summary.stage_metrics.get(stage)
|
||||||
assert actual is not None, f"Stage {stage} timing missing"
|
assert actual is not None, f"Stage {stage} timing missing"
|
||||||
tolerance = (
|
tolerance = self._timing_tol(
|
||||||
self.tolerances.denoise_stage
|
self.tolerances.denoise_stage
|
||||||
if stage == "DenoisingStage"
|
if stage == "DenoisingStage"
|
||||||
else self.tolerances.non_denoise_stage
|
else self.tolerances.non_denoise_stage
|
||||||
@@ -822,7 +834,7 @@ class VideoPerformanceValidator(PerformanceValidator):
|
|||||||
f"Denoise Step {idx}",
|
f"Denoise Step {idx}",
|
||||||
actual,
|
actual,
|
||||||
expected,
|
expected,
|
||||||
FIRST_DENOISE_STEP_TOLERANCE,
|
self._timing_tol(FIRST_DENOISE_STEP_TOLERANCE),
|
||||||
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
|
min_abs_tolerance=FIRST_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -833,7 +845,7 @@ class VideoPerformanceValidator(PerformanceValidator):
|
|||||||
f"Denoise Step {idx}",
|
f"Denoise Step {idx}",
|
||||||
actual,
|
actual,
|
||||||
expected,
|
expected,
|
||||||
self.tolerances.denoise_step,
|
self._timing_tol(self.tolerances.denoise_step),
|
||||||
min_abs_tolerance=VIDEO_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
|
min_abs_tolerance=VIDEO_DENOISE_STEP_MIN_ABS_TOLERANCE_MS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -863,7 +875,7 @@ class VideoPerformanceValidator(PerformanceValidator):
|
|||||||
"Average Frame Time",
|
"Average Frame Time",
|
||||||
summary.avg_frame_time_ms,
|
summary.avg_frame_time_ms,
|
||||||
expected_frame_time,
|
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).
|
# Anonymous-host budget caps; None skips the check (older baselines).
|
||||||
load_peak_host_anon_mb: float | None = None
|
load_peak_host_anon_mb: float | None = None
|
||||||
runtime_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
|
@classmethod
|
||||||
def from_dict(cls, cfg: dict[str, Any]) -> ScenarioConfig:
|
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"),
|
runtime_peak_allocated_mb=optional_float("runtime_peak_allocated_mb"),
|
||||||
load_peak_host_anon_mb=optional_float("load_peak_host_anon_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"),
|
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(_warmup_req())
|
||||||
worker._release_warmup_pool(SimpleNamespace(is_warmup=False, extra={}))
|
worker._release_warmup_pool(SimpleNamespace(is_warmup=False, extra={}))
|
||||||
assert calls == []
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user