Files
2026-09-23 22:06:58 -07:00

282 lines
11 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Test cleanup helpers (device memory, distributed state, and memory monitoring).
``vllm_omni.platforms`` is imported only inside functions that need it so importing this module
at pytest plugin load does not run before session autouse fixtures.
"""
from __future__ import annotations
import gc
import logging
import os
import subprocess
import time
from vllm_omni.platforms import current_omni_platform
logger = logging.getLogger(__name__)
def get_physical_device_indices(devices):
"""Map logical device indices to physical IDs via the platform visibility env.
Uses ``current_omni_platform.device_control_env_var`` (e.g. ``CUDA_VISIBLE_DEVICES``,
``ASCEND_RT_VISIBLE_DEVICES``) rather than hard-coding CUDA.
"""
visible_devices = os.environ.get(current_omni_platform.device_control_env_var)
if visible_devices is None:
return devices
visible_indices = [int(x) for x in visible_devices.split(",") if x.strip() != ""]
index_mapping = {i: physical for i, physical in enumerate(visible_indices)}
return [index_mapping[i] for i in devices if i in index_mapping]
def pick_least_used_device_indices(num_devices: int) -> list[int]:
"""Return physical device indices for the least-used accelerators.
Args:
num_devices: Number of device indices to return.
Returns:
Physical device indices sorted from least to most used memory.
"""
if num_devices <= 0:
raise ValueError(f"num_devices must be positive, got {num_devices}.")
logical_count = current_omni_platform.get_device_count()
if logical_count < num_devices:
raise RuntimeError(f"Need {num_devices} devices, but only {logical_count} are available.")
device_usage: list[tuple[int, int]] = []
for device_index in range(logical_count):
with current_omni_platform.device(device_index):
free_bytes, total_bytes = current_omni_platform.get_device_memory()
device_usage.append((total_bytes - free_bytes, device_index))
device_usage.sort()
logical_indices = [device_index for _, device_index in device_usage[:num_devices]]
return get_physical_device_indices(logical_indices)
def _logical_to_physical(logical: int) -> int:
"""Physical id for logs / nvidia-smi; ``logical`` if the env does not map it."""
mapped = get_physical_device_indices([logical])
return mapped[0] if mapped else logical
def wait_for_gpu_memory_to_clear(
*,
devices: list[int],
threshold_bytes: int | None = None,
threshold_ratio: float | None = None,
timeout_s: float = 120,
) -> None:
"""Wait until *logical* device ordinals are under the memory threshold.
``devices`` are CUDA/NPU ordinals in the current process (``0..N-1``),
the same space as ``current_omni_platform.device()`` / ``mem_get_info()``.
Physical ids (``CUDA_VISIBLE_DEVICES``) are only used for log labels and
Whisper-worker matching — remapping them into ``device()`` raises
``invalid device ordinal`` when the visible set is a subset (e.g. ``4,5``).
"""
assert threshold_bytes is not None or threshold_ratio is not None
logical_devices = list(devices)
physical_by_logical = {logical: _logical_to_physical(logical) for logical in logical_devices}
start_time = time.time()
device_list = ", ".join(str(physical_by_logical[d]) for d in logical_devices)
if threshold_bytes is not None:
condition_str = f"Memory usage ≤ {threshold_bytes / 2**30:.2f} GiB (excl. Whisper worker)"
def is_free(used, total):
return used <= threshold_bytes / 2**30
else:
ratio = threshold_ratio
assert ratio is not None
condition_str = f"Memory usage ratio ≤ {ratio * 100:.1f}% (excl. Whisper worker)"
def is_free(used, total):
return used / total <= ratio
print(f"[Device Memory Monitor] Waiting for device(s) {device_list} to free memory, Condition: {condition_str}")
whisper_allowance_gib, whisper_physical = _whisper_vram_allowance()
if whisper_allowance_gib > 0 and whisper_physical is not None:
print(
f"[Device Memory Monitor] Whisper worker allowance {whisper_allowance_gib:.1f}GiB "
f"on device {whisper_physical}"
)
while True:
output_raw: dict[int, tuple[float, float, float]] = {}
for logical in logical_devices:
with current_omni_platform.device(logical):
free_bytes, total_bytes = current_omni_platform.mem_get_info()
used_gib = (total_bytes - free_bytes) / 2**30
total_gib = total_bytes / 2**30
physical = physical_by_logical[logical]
whisper_gib = (
min(used_gib, whisper_allowance_gib)
if whisper_physical is not None and physical == whisper_physical
else 0.0
)
output_raw[physical] = (max(0.0, used_gib - whisper_gib), total_gib, whisper_gib)
print("[Device Memory Status] Current usage (engine view excludes Whisper worker):")
for device_id, (used, total, whisper) in output_raw.items():
ratio_pct = (used / total) * 100 if total > 0 else 0.0
line = f" Device {device_id}: {used:.1f}GiB/{total:.1f}GiB ({ratio_pct:.1f}%)"
if whisper > 0:
line += f" [Whisper worker {whisper:.1f}GiB excluded]"
print(line)
dur_s = time.time() - start_time
if all(is_free(used, total) for used, total, _whisper in output_raw.values()):
print(f"[Device Memory Freed] Device(s) {device_list} meet memory condition")
print(f" Condition: {condition_str}")
print(f" Wait time: {dur_s:.1f} seconds ({dur_s / 60:.1f} minutes)")
break
if dur_s >= timeout_s:
status = "\n".join(
f" Device {d}: {used:.1f}GiB/{total:.1f}GiB"
+ (f" (+Whisper {whisper:.1f}GiB excluded)" if whisper > 0 else "")
for d, (used, total, whisper) in output_raw.items()
)
raise ValueError(
f"[Device Memory Timeout] Device(s) {device_list} still don't meet memory condition after {dur_s:.1f} seconds\n"
f"Condition: {condition_str}\n"
f"Current status:\n{status}"
)
gc.collect()
current_omni_platform.empty_cache()
time.sleep(5)
def _whisper_vram_allowance() -> tuple[float, int | None]:
"""Living Whisper worker's GPU footprint and physical device.
Orthogonal to RFC #6851 leftover-engine PID reap / a raised wait ratio:
this only excludes the transcriber's own allocator (capped by current used
on that device). CPU / unknown device → ``(0.0, None)``.
"""
try:
from tests.helpers.media import whisper_resident_device_index, whisper_resident_vram_gib
except Exception:
return 0.0, None
gib = whisper_resident_vram_gib()
logical = whisper_resident_device_index()
if gib <= 0.0 or logical is None:
return 0.0, None
mapped = get_physical_device_indices([logical])
if not mapped:
return 0.0, None
return gib, mapped[0]
def _run_smi(label: str, cmd: list[str], head_lines: int, timeout: float = 5) -> None:
print("\n" + "=" * 80)
print(label)
print("=" * 80)
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
if result.returncode == 0 and result.stdout.strip():
lines = result.stdout.strip().split("\n")
for line in lines[:head_lines]:
print(line)
if len(lines) > head_lines:
print(f"... (showing first {head_lines} of {len(lines)} lines)")
else:
print(f"{cmd[0]} command failed or produced no output")
except (subprocess.TimeoutExpired, FileNotFoundError):
print(f"{cmd[0]} not available or timed out")
except Exception as e:
print(f"Error running {' '.join(cmd)}: {e}")
def _print_device_processes() -> None:
"""Print device information via platform SMI tools."""
if current_omni_platform.is_cuda():
_run_smi("NVIDIA GPU Information (nvidia-smi)", ["nvidia-smi"], 20)
_run_smi("Detailed GPU Processes (nvidia-smi pmon)", ["nvidia-smi", "pmon", "-c", "1"], 100, timeout=3)
elif current_omni_platform.is_npu():
_run_smi("Ascend NPU Information (npu-smi info)", ["npu-smi", "info"], 40)
elif current_omni_platform.is_rocm():
# amd-smi can enter uninterruptible sleep in the amdgpu CPER ioctl
# (amdgpu_cper_ring_write). In that state subprocess.run's timeout
# cannot reap it, causing this diagnostic hook to hang the entire test.
# rocm-smi provides the information needed here without that ioctl.
_run_smi(
"AMD GPU Information (rocm-smi)",
["rocm-smi", "--showuse", "--showmeminfo", "vram"],
60,
)
_run_smi("Detailed AMD GPU Processes (rocm-smi)", ["rocm-smi", "--showpids"], 100, timeout=3)
elif current_omni_platform.is_xpu():
_run_smi("Intel XPU Information (xpu-smi discovery)", ["xpu-smi", "discovery"], 40)
elif current_omni_platform.is_musa():
_run_smi("Moore Threads GPU Information (mthreads-gmi)", ["mthreads-gmi"], 30)
else:
print("\n" + "=" * 80)
print("WARNING: No supported device platform detected")
print("=" * 80)
def cleanup_test_environment(*, shutdown_ray: bool = False) -> None:
"""Tear down distributed state and reset device memory for tests."""
from vllm import envs
from vllm.distributed.parallel_state import (
destroy_distributed_environment,
destroy_model_parallel,
)
envs.disable_envs_cache()
if current_omni_platform.is_rocm():
from vllm._aiter_ops import rocm_aiter_ops
rocm_aiter_ops.refresh_env_variables()
gc.unfreeze()
destroy_model_parallel()
destroy_distributed_environment()
if shutdown_ray:
import ray
ray.shutdown()
print("Pre-test device status:")
gc.collect()
if not current_omni_platform.is_cpu():
current_omni_platform.empty_cache()
try:
import torch
torch._C._host_emptyCache()
except AttributeError:
logger.warning("torch._C._host_emptyCache() only available in Pytorch >=2.5")
num_devices = current_omni_platform.device_count()
if num_devices > 0:
try:
wait_for_gpu_memory_to_clear(
devices=list(range(num_devices)),
threshold_ratio=0.05,
timeout_s=60,
)
except Exception as e:
print(f"Device cleanup note: {e}")
if current_omni_platform.is_available():
print("Post-test device status:")
_print_device_processes()
__all__ = [
"cleanup_test_environment",
"get_physical_device_indices",
"pick_least_used_device_indices",
"wait_for_gpu_memory_to_clear",
]