[CI] Wait for GPU memory release before each test class setUpClass (#31509)
This commit is contained in:
@@ -2177,6 +2177,94 @@ def maybe_stub_sgl_kernel():
|
|||||||
sys.meta_path.insert(0, _SglKernelFinder())
|
sys.meta_path.insert(0, _SglKernelFinder())
|
||||||
|
|
||||||
|
|
||||||
|
_GPU_IDLE_TIMEOUT_SECS = 30.0
|
||||||
|
_GPU_IDLE_POLL_INTERVAL_SECS = 2.0
|
||||||
|
_GPU_IDLE_USED_MEMORY_THRESHOLD = 2 << 30 # 2 GiB
|
||||||
|
|
||||||
|
|
||||||
|
def _format_gib(num_bytes: Optional[int]) -> str:
|
||||||
|
if num_bytes is None:
|
||||||
|
return "N/A"
|
||||||
|
return f"{num_bytes / (1 << 30):.2f} GiB"
|
||||||
|
|
||||||
|
|
||||||
|
def _visible_gpu_indices(pynvml) -> List[int]:
|
||||||
|
num_gpus = pynvml.nvmlDeviceGetCount()
|
||||||
|
visible = os.environ.get("CUDA_VISIBLE_DEVICES")
|
||||||
|
if visible is None:
|
||||||
|
return list(range(num_gpus))
|
||||||
|
entries = [entry.strip() for entry in visible.split(",") if entry.strip()]
|
||||||
|
if not all(entry.isdigit() for entry in entries):
|
||||||
|
# UUID-style entries; fall back to checking all GPUs.
|
||||||
|
return list(range(num_gpus))
|
||||||
|
return [int(entry) for entry in entries if int(entry) < num_gpus]
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_busy_gpu_reports(pynvml, gpu_indices: List[int]) -> List[str]:
|
||||||
|
reports = []
|
||||||
|
for index in gpu_indices:
|
||||||
|
handle = pynvml.nvmlDeviceGetHandleByIndex(index)
|
||||||
|
used_bytes = pynvml.nvmlDeviceGetMemoryInfo(handle).used
|
||||||
|
if used_bytes < _GPU_IDLE_USED_MEMORY_THRESHOLD:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
procs = pynvml.nvmlDeviceGetComputeRunningProcesses(handle)
|
||||||
|
proc_info = ", ".join(
|
||||||
|
f"pid={proc.pid} {_format_gib(proc.usedGpuMemory)}" for proc in procs
|
||||||
|
)
|
||||||
|
except pynvml.NVMLError:
|
||||||
|
proc_info = ""
|
||||||
|
reports.append(
|
||||||
|
f"GPU {index} uses {_format_gib(used_bytes)}"
|
||||||
|
f" ({proc_info or 'no compute processes found'})"
|
||||||
|
)
|
||||||
|
return reports
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_gpu_idle_in_ci(
|
||||||
|
timeout: float = _GPU_IDLE_TIMEOUT_SECS,
|
||||||
|
poll_interval: float = _GPU_IDLE_POLL_INTERVAL_SECS,
|
||||||
|
) -> None:
|
||||||
|
"""Wait until visible GPUs release residual memory from earlier tests.
|
||||||
|
|
||||||
|
Killed server processes return GPU memory asynchronously; launching the
|
||||||
|
next server too early makes memory profiling over-commit the KV cache and
|
||||||
|
OOM. Abort with the offending processes if the memory is never returned.
|
||||||
|
"""
|
||||||
|
if not is_in_ci():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
import pynvml
|
||||||
|
|
||||||
|
pynvml.nvmlInit()
|
||||||
|
except Exception:
|
||||||
|
# Non-NVIDIA runner (CPU/AMD) or NVML unavailable; nothing to check.
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
gpu_indices = _visible_gpu_indices(pynvml)
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while True:
|
||||||
|
busy_reports = _collect_busy_gpu_reports(pynvml, gpu_indices)
|
||||||
|
if not busy_reports:
|
||||||
|
return
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"GPU(s) still not idle after waiting {timeout:.0f}s "
|
||||||
|
f"before setUpClass: {'; '.join(busy_reports)}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[CI GPU Idle] Waiting for GPU to become idle: "
|
||||||
|
f"{'; '.join(busy_reports)}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
pynvml.nvmlShutdown()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CustomTestCase(unittest.TestCase):
|
class CustomTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
@@ -2193,6 +2281,7 @@ class CustomTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def safe_setUpClass(klass):
|
def safe_setUpClass(klass):
|
||||||
try:
|
try:
|
||||||
|
_wait_for_gpu_idle_in_ci()
|
||||||
orig_func(klass)
|
orig_func(klass)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Best-effort cleanup; suppress teardown errors so the
|
# Best-effort cleanup; suppress teardown errors so the
|
||||||
|
|||||||
Reference in New Issue
Block a user