|
25 | 25 | import itertools |
26 | 26 | import logging |
27 | 27 | import os |
| 28 | +import time |
28 | 29 | from typing import Coroutine, Optional |
29 | 30 |
|
30 | 31 | import torch |
@@ -252,14 +253,71 @@ def tokenizer(self): |
252 | 253 | return self._tokenizer |
253 | 254 |
|
254 | 255 | def cleanup(self): |
255 | | - destroy_model_parallel() |
| 256 | + # Explicitly shut down the vLLM engine so that its GPU memory is released before |
| 257 | + # any subsequently loaded engine (e.g. the vLLM LLM-as-judge used by some metrics) |
| 258 | + # tries to allocate. Relying on `del` alone is not enough: with the vLLM V1 engine |
| 259 | + # the worker keeps the allocation until the engine core is shut down, and that |
| 260 | + # teardown can be asynchronous -- so we also wait until the memory is reclaimed. |
256 | 261 | if self.model is not None: |
257 | | - del self.model |
| 262 | + try: |
| 263 | + engine_core = getattr(getattr(self.model, "llm_engine", None), "engine_core", None) |
| 264 | + if engine_core is not None and hasattr(engine_core, "shutdown"): |
| 265 | + engine_core.shutdown() |
| 266 | + else: |
| 267 | + logger.warning( |
| 268 | + "Could not find a vLLM engine_core to shut down explicitly " |
| 269 | + "(V0 engine or unsupported vLLM version). GPU memory may not be " |
| 270 | + "released before the next engine loads." |
| 271 | + ) |
| 272 | + except Exception as e: |
| 273 | + logger.warning(f"Could not explicitly shut down the vLLM engine: {e}") |
| 274 | + |
| 275 | + destroy_model_parallel() |
| 276 | + self.model = None # drops the only strong reference to the LLM object |
258 | 277 | gc.collect() |
259 | 278 | ray.shutdown() |
260 | 279 | destroy_distributed_environment() |
261 | 280 | torch.cuda.empty_cache() |
262 | 281 |
|
| 282 | + # Wait until the GPU memory is actually freed (engine teardown may be asynchronous), |
| 283 | + # so the next engine to load sees a clean device instead of failing to allocate. |
| 284 | + # The free-memory heuristic does not work when the GPU is shared with another process |
| 285 | + # or on hardware with unified memory (e.g. DGX Spark). Set |
| 286 | + # LIGHTEVAL_VLLM_SKIP_MEMORY_WAIT=1 to disable the wait in those cases. |
| 287 | + if os.environ.get("LIGHTEVAL_VLLM_SKIP_MEMORY_WAIT"): |
| 288 | + return |
| 289 | + if not torch.cuda.is_available(): |
| 290 | + return |
| 291 | + |
| 292 | + timeout_s = 60 |
| 293 | + threshold = 0.7 |
| 294 | + device_count = torch.cuda.device_count() |
| 295 | + per_device = [] |
| 296 | + for _ in range(timeout_s): |
| 297 | + per_device = [torch.cuda.mem_get_info(d) for d in range(device_count)] |
| 298 | + valid = [(free, total) for free, total in per_device if total > 0] |
| 299 | + if not valid: |
| 300 | + return |
| 301 | + # Use the *minimum* free-ratio across devices so we do not exit early when |
| 302 | + # only device 0 has been reclaimed while a tensor-parallel peer is still busy. |
| 303 | + if min(free / total for free, total in valid) > threshold: |
| 304 | + return |
| 305 | + gc.collect() |
| 306 | + torch.cuda.empty_cache() |
| 307 | + time.sleep(1) |
| 308 | + |
| 309 | + usage_str = ", ".join( |
| 310 | + f"GPU{d}: {free / total:.0%} free" if total > 0 else f"GPU{d}: n/a" |
| 311 | + for d, (free, total) in enumerate(per_device) |
| 312 | + ) |
| 313 | + logger.warning( |
| 314 | + f"vLLM GPU memory was not fully reclaimed within {timeout_s}s after engine " |
| 315 | + f"shutdown (threshold {threshold:.0%} free; current state: {usage_str}). " |
| 316 | + "The next engine to load may OOM. " |
| 317 | + "To skip this wait (e.g. on a GPU shared with another process, or on hardware " |
| 318 | + "with unified memory like DGX Spark), set LIGHTEVAL_VLLM_SKIP_MEMORY_WAIT=1." |
| 319 | + ) |
| 320 | + |
263 | 321 | @property |
264 | 322 | def add_special_tokens(self): |
265 | 323 | return self._add_special_tokens |
|
0 commit comments