[diffusion] CI: guard the anonymous-host budget alongside peak VRAM (#36051)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mick
2026-08-23 20:13:46 +08:00
committed by GitHub
co-authored by Claude Fable 5
parent dd15fb57b5
commit 886e37a649
6 changed files with 141 additions and 1 deletions
@@ -240,6 +240,15 @@ def safetensors_weights_iterator(
if use_runai_model_streamer if use_runai_model_streamer
else FALLBACK_READER.name else FALLBACK_READER.name
) )
elif to_cpu:
# A host-bound load keeps the checkpoint mapping: mapped pages are the
# zero-copy optimum there, and everything downstream that budgets host
# memory (layerwise offload, pinning, the mapped-weight gate) assumes
# them. The streamer materializes anonymous copies instead -- measured
# as the whole 61.7 GB DiT landing in host anon on the 5090 CI runner
# -- and its strengths (direct-to-GPU, remote streaming) do not apply
# to a local file headed for the CPU.
requested = FALLBACK_READER.name
backend = select_weight_reader( backend = select_weight_reader(
requested=requested, needs_key_filter=key_filter is not None requested=requested, needs_key_filter=key_filter is not None
) )
@@ -5,6 +5,7 @@ import logging
import os import os
import subprocess import subprocess
import sys import sys
import threading
import time import time
from datetime import datetime from datetime import datetime
from functools import lru_cache from functools import lru_cache
@@ -34,6 +35,10 @@ class MemorySnapshot:
reserved_mb: float # current reserved memory (actual VRAM) reserved_mb: float # current reserved memory (actual VRAM)
peak_allocated_mb: float # peak allocated since last reset peak_allocated_mb: float # peak allocated since last reset
peak_reserved_mb: float # peak reserved since last reset peak_reserved_mb: float # peak reserved since last reset
# Peak anonymous host memory (RssAnon) sampled by the worker. Anonymous,
# not RSS: file-backed pages the kernel can drop are not a budget cost.
# 0.0 where no sampler ran (non-Linux, or an old record).
peak_host_anon_mb: float = 0.0
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
@@ -41,6 +46,7 @@ class MemorySnapshot:
"reserved_mb": round(self.reserved_mb, 2), "reserved_mb": round(self.reserved_mb, 2),
"peak_allocated_mb": round(self.peak_allocated_mb, 2), "peak_allocated_mb": round(self.peak_allocated_mb, 2),
"peak_reserved_mb": round(self.peak_reserved_mb, 2), "peak_reserved_mb": round(self.peak_reserved_mb, 2),
"peak_host_anon_mb": round(self.peak_host_anon_mb, 2),
} }
@@ -125,6 +131,58 @@ def get_git_commit_hash() -> str:
return "N/A" return "N/A"
class _HostAnonSampler:
"""Tracks this process's peak anonymous host memory (RssAnon).
The kernel keeps a high-water mark for RSS (VmHWM) but none for the
anonymous share, and the anonymous share is the budget cost: file-backed
pages are droppable and come back on their own. A 1 s sampling thread is
enough resolution for weight-sized (GiB, seconds-long) growth.
"""
def __init__(self) -> None:
self._peak_kb = 0
self._started = False
self._lock = threading.Lock()
def _read_kb(self) -> int:
try:
with open("/proc/self/status") as handle:
for line in handle:
if line.startswith("RssAnon:"):
return int(line.split()[1])
except OSError:
pass
return 0
def _run(self) -> None:
while True:
value = self._read_kb()
if value > self._peak_kb:
self._peak_kb = value
time.sleep(1.0)
def _ensure_started(self) -> None:
if self._started:
return
with self._lock:
if self._started:
return
self._started = True
if self._read_kb() == 0:
return # no /proc on this platform; peak stays 0
threading.Thread(
target=self._run, name="host-anon-sampler", daemon=True
).start()
def peak_mb(self) -> float:
self._ensure_started()
return max(self._peak_kb, self._read_kb()) / 1024.0
_host_anon_sampler = _HostAnonSampler()
def capture_memory_snapshot() -> MemorySnapshot: def capture_memory_snapshot() -> MemorySnapshot:
if not torch.get_device_module().is_available(): if not torch.get_device_module().is_available():
return MemorySnapshot( return MemorySnapshot(
@@ -132,6 +190,7 @@ def capture_memory_snapshot() -> MemorySnapshot:
reserved_mb=0.0, reserved_mb=0.0,
peak_allocated_mb=0.0, peak_allocated_mb=0.0,
peak_reserved_mb=0.0, peak_reserved_mb=0.0,
peak_host_anon_mb=_host_anon_sampler.peak_mb(),
) )
if current_platform.is_mps(): if current_platform.is_mps():
@@ -142,6 +201,7 @@ def capture_memory_snapshot() -> MemorySnapshot:
reserved_mb=reserved / (1024**2), reserved_mb=reserved / (1024**2),
peak_allocated_mb=allocated / (1024**2), peak_allocated_mb=allocated / (1024**2),
peak_reserved_mb=reserved / (1024**2), peak_reserved_mb=reserved / (1024**2),
peak_host_anon_mb=_host_anon_sampler.peak_mb(),
) )
allocated = torch.get_device_module().memory_allocated() allocated = torch.get_device_module().memory_allocated()
@@ -154,6 +214,7 @@ def capture_memory_snapshot() -> MemorySnapshot:
reserved_mb=reserved / (1024**2), reserved_mb=reserved / (1024**2),
peak_allocated_mb=peak_allocated / (1024**2), peak_allocated_mb=peak_allocated / (1024**2),
peak_reserved_mb=peak_reserved / (1024**2), peak_reserved_mb=peak_reserved / (1024**2),
peak_host_anon_mb=_host_anon_sampler.peak_mb(),
) )
@@ -155,6 +155,8 @@
"expected_median_denoise_ms": 11000.0, "expected_median_denoise_ms": 11000.0,
"load_peak_vram_mb": 6000.0, "load_peak_vram_mb": 6000.0,
"runtime_peak_vram_mb": 12288.0, "runtime_peak_vram_mb": 12288.0,
"load_peak_host_anon_mb": 32768.0,
"runtime_peak_host_anon_mb": 32768.0,
"estimated_full_test_time_s": 1050.0 "estimated_full_test_time_s": 1050.0
} }
} }
@@ -467,8 +467,13 @@ class DiffusionServerBase:
expected_load_peak_vram_mb, expected_load_peak_vram_mb,
expected_runtime_peak_vram_mb, expected_runtime_peak_vram_mb,
) )
validator.validate_peak_host_anon(
summary,
scenario.load_peak_host_anon_mb,
scenario.runtime_peak_host_anon_mb,
)
except AssertionError as e: except AssertionError as e:
logger.error(f"Peak VRAM validation failed for {case.id}:\n{e}") logger.error(f"Peak memory validation failed for {case.id}:\n{e}")
self._dump_baseline_for_testcase(case, summary, missing_scenario) self._dump_baseline_for_testcase(case, summary, missing_scenario)
raise raise
@@ -664,6 +669,10 @@ class DiffusionServerBase:
{ {
"load_peak_vram_mb": round(summary.load_peak_vram_mb, 2), "load_peak_vram_mb": round(summary.load_peak_vram_mb, 2),
"runtime_peak_vram_mb": round(summary.runtime_peak_vram_mb, 2), "runtime_peak_vram_mb": round(summary.runtime_peak_vram_mb, 2),
"load_peak_host_anon_mb": round(summary.load_peak_host_anon_mb, 2),
"runtime_peak_host_anon_mb": round(
summary.runtime_peak_host_anon_mb, 2
),
} }
) )
@@ -610,6 +610,46 @@ class PerformanceValidator:
unit=" MiB", unit=" MiB",
) )
def validate_peak_host_anon(
self,
summary: PerformanceSummary,
expected_load_mb: float | None,
expected_runtime_mb: float | None,
) -> None:
"""Anonymous-host budget: peaks must stay at or under the baseline.
Skipped wholesale when the baseline carries no host figures (older
scenarios) or the record has none (non-Linux, or a server predating
the sampler) -- the VRAM checks do not imply anything about the host,
as the LoRA-merge blow-up showed: VRAM green, host budget gone.
"""
if expected_load_mb is None and expected_runtime_mb is None:
return
if summary.runtime_peak_host_anon_mb <= 0:
logger.warning(
"Host-anon baseline present but the record has no host peaks; "
"skipping the host budget check"
)
return
if expected_load_mb is not None:
self._assert_le(
"Load Peak Host Anon",
summary.load_peak_host_anon_mb,
expected_load_mb,
self.tolerances.host_anon,
min_abs_tolerance=256.0,
unit=" MiB",
)
if expected_runtime_mb is not None:
self._assert_le(
"Runtime Peak Host Anon",
summary.runtime_peak_host_anon_mb,
expected_runtime_mb,
self.tolerances.host_anon,
min_abs_tolerance=256.0,
unit=" MiB",
)
def validate( def validate(
self, perf_record: RequestPerfRecord, *args, **kwargs self, perf_record: RequestPerfRecord, *args, **kwargs
) -> PerformanceSummary: ) -> PerformanceSummary:
@@ -48,6 +48,7 @@ class ToleranceConfig:
denoise_agg: float denoise_agg: float
load_peak_vram: float = 0.01 load_peak_vram: float = 0.01
runtime_peak_vram: float = 0.02 runtime_peak_vram: float = 0.02
host_anon: float = 0.02
@classmethod @classmethod
def load_profile(cls, all_tolerances: dict, profile_name: str) -> ToleranceConfig: def load_profile(cls, all_tolerances: dict, profile_name: str) -> ToleranceConfig:
@@ -100,6 +101,7 @@ class ToleranceConfig:
tol_data.get("runtime_peak_vram", 0.02), tol_data.get("runtime_peak_vram", 0.02),
) )
), ),
host_anon=float(tol_data.get("host_anon", 0.02)),
) )
@@ -115,6 +117,9 @@ class ScenarioConfig:
estimated_full_test_time_s: float | None = None estimated_full_test_time_s: float | None = None
load_peak_vram_mb: float | None = None load_peak_vram_mb: float | None = None
runtime_peak_vram_mb: float | None = None runtime_peak_vram_mb: float | None = None
# 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
@classmethod @classmethod
def from_dict(cls, cfg: dict[str, Any]) -> ScenarioConfig: def from_dict(cls, cfg: dict[str, Any]) -> ScenarioConfig:
@@ -131,6 +136,8 @@ class ScenarioConfig:
estimated_full_test_time_s=optional_float("estimated_full_test_time_s"), estimated_full_test_time_s=optional_float("estimated_full_test_time_s"),
load_peak_vram_mb=optional_float("load_peak_vram_mb"), load_peak_vram_mb=optional_float("load_peak_vram_mb"),
runtime_peak_vram_mb=optional_float("runtime_peak_vram_mb"), runtime_peak_vram_mb=optional_float("runtime_peak_vram_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"),
) )
@@ -438,6 +445,8 @@ class PerformanceSummary:
all_denoise_steps: dict[int, float] all_denoise_steps: dict[int, float]
load_peak_vram_mb: float = 0.0 load_peak_vram_mb: float = 0.0
runtime_peak_vram_mb: float = 0.0 runtime_peak_vram_mb: float = 0.0
load_peak_host_anon_mb: float = 0.0
runtime_peak_host_anon_mb: float = 0.0
frames_per_second: float | None = None frames_per_second: float | None = None
total_frames: int | None = None total_frames: int | None = None
avg_frame_time_ms: float | None = None avg_frame_time_ms: float | None = None
@@ -473,6 +482,14 @@ class PerformanceSummary:
runtime_peak_vram_mb = float( runtime_peak_vram_mb = float(
record.memory_snapshots.get("runtime_peak", {}).get("peak_reserved_mb", 0.0) record.memory_snapshots.get("runtime_peak", {}).get("peak_reserved_mb", 0.0)
) )
load_peak_host_anon_mb = float(
record.memory_snapshots.get("load_peak", {}).get("peak_host_anon_mb", 0.0)
)
runtime_peak_host_anon_mb = float(
record.memory_snapshots.get("runtime_peak", {}).get(
"peak_host_anon_mb", 0.0
)
)
return PerformanceSummary( return PerformanceSummary(
e2e_ms=e2e_ms, e2e_ms=e2e_ms,
@@ -484,6 +501,8 @@ class PerformanceSummary:
all_denoise_steps=per_step, all_denoise_steps=per_step,
load_peak_vram_mb=load_peak_vram_mb, load_peak_vram_mb=load_peak_vram_mb,
runtime_peak_vram_mb=runtime_peak_vram_mb, runtime_peak_vram_mb=runtime_peak_vram_mb,
load_peak_host_anon_mb=load_peak_host_anon_mb,
runtime_peak_host_anon_mb=runtime_peak_host_anon_mb,
) )