dynamo/lib/gpu_memory_service/integrations/vllm/worker.py

174 lines
6.5 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""GPU Memory Service Worker subclass for vLLM integration.
This module provides a custom Worker class that properly integrates with
GPU Memory Service for VA-stable weight sharing and unmap/remap functionality.
Usage:
Set --worker-cls=gpu_memory_service.integrations.vllm.worker:GMSWorker
"""
from __future__ import annotations
import logging
from contextlib import nullcontext
from typing import List, Optional
import torch
from gpu_memory_service import (
get_gms_client_memory_manager,
get_or_create_gms_client_memory_manager,
)
from gpu_memory_service.common.types import RequestedLockType
from gpu_memory_service.common.utils import get_socket_path
from gpu_memory_service.integrations.common import patch_empty_cache
from gpu_memory_service.integrations.common.utils import get_gms_lock_mode
from gpu_memory_service.integrations.vllm.model_loader import register_gms_loader
from gpu_memory_service.integrations.vllm.patches import patch_memory_snapshot
logger = logging.getLogger(__name__)
# Trigger model loader registration and utility patches on import
register_gms_loader()
patch_empty_cache()
patch_memory_snapshot()
logger.info(
"[GMS] Worker module loaded - model loader registered, utility patches applied"
)
# Import Worker after patches are applied
from vllm.v1.worker.gpu_worker import Worker # noqa: E402
class GMSWorker(Worker):
"""vLLM Worker subclass with GMS integration."""
def init_device(self) -> None:
"""Initialize device with early GMS connection.
We set CUDA device and establish GMS connection BEFORE calling super()
so that MemorySnapshot.measure can query committed bytes.
"""
from vllm.platforms import current_platform
# Set CUDA device first (vLLM provides self.local_rank)
device = self.local_rank
current_platform.set_device(torch.device(f"cuda:{device}"))
# Establish weights GMS connection (so MemorySnapshot can query committed bytes).
# Fetch extra config from vLLM load_config to determine RW/RO lock mode.
extra = (
getattr(self.vllm_config.load_config, "model_loader_extra_config", {}) or {}
)
socket_path = get_socket_path(device)
get_or_create_gms_client_memory_manager(
socket_path,
device,
mode=get_gms_lock_mode(extra),
tag="weights",
)
# Parent will set device again (harmless) and do memory checks
super().init_device()
def load_model(self, *args, **kwargs) -> None:
"""Load model with corrected memory accounting.
After the parent loads the model, we correct the model_memory_usage
to reflect the actual bytes imported from GMS (not the delta measured
by vLLM's memory tracking).
"""
super().load_model(*args, **kwargs)
# Correct memory accounting for GMS-imported weights
try:
from gpu_memory_service.integrations.vllm.model_loader import (
get_imported_weights_bytes,
)
imported_bytes = int(get_imported_weights_bytes())
if (
imported_bytes > 0
and hasattr(self, "model_runner")
and self.model_runner is not None
):
old_usage = getattr(self.model_runner, "model_memory_usage", 0)
self.model_runner.model_memory_usage = imported_bytes
logger.info(
"[GMS] Corrected model_memory_usage: %.2f GiB -> %.2f GiB",
old_usage / (1 << 30),
imported_bytes / (1 << 30),
)
except Exception as e:
logger.debug("[GMS] Could not correct memory accounting: %s", e)
def sleep(self, level: int = 1) -> None:
"""
vLLM sleep implementation with GMS integration.
NOTE: `level` is a no-op here: weights are only unmapped (but remain in GPU memory).
NOTE: We do NOT call super().sleep() because it tries to copy GPU buffers to CPU,
which segfaults on already-unmapped GMS memory.
"""
free_bytes_before = torch.cuda.mem_get_info()[0]
# Unmap GMS weights: synchronize + unmap all VAs + disconnect
manager = get_gms_client_memory_manager()
assert manager is not None, "GMS client is not initialized"
assert not manager.is_unmapped, "GMS weights are already unmapped"
manager.unmap_all_vas()
manager.disconnect()
# Sleep KV cache via CuMemAllocator
from vllm.device_allocator.cumem import CuMemAllocator
allocator = CuMemAllocator.get_instance()
allocator.sleep(offload_tags=tuple())
free_bytes_after, total = torch.cuda.mem_get_info()
freed_bytes = free_bytes_after - free_bytes_before
used_bytes = total - free_bytes_after
logger.info(
"Sleep freed %.2f GiB, %.2f GiB still in use.",
freed_bytes / (1 << 30),
used_bytes / (1 << 30),
)
def wake_up(self, tags: Optional[List[str]] = None) -> None:
"""vLLM wake implementation with GMS integration."""
if tags is None:
tags = ["weights", "kv_cache"]
if "weights" in tags:
manager = get_gms_client_memory_manager()
assert manager is not None, "GMS client is not initialized"
assert manager.is_unmapped, "GMS weights are not unmapped"
manager.connect(RequestedLockType.RO)
manager.remap_all_vas()
if "kv_cache" in tags:
from vllm.device_allocator.cumem import CuMemAllocator
allocator = CuMemAllocator.get_instance()
allocator.wake_up(tags=["kv_cache"])
# Reinitialize FP8 KV scales if needed
if self.cache_config.cache_dtype.startswith("fp8") and hasattr(
self.model_runner, "init_fp8_kv_scales"
):
self.model_runner.init_fp8_kv_scales()
def _maybe_get_memory_pool_context(self, tag: str):
"""Skip CuMemAllocator for weights when using GMS.
GMS manages its own memory pool for weights, so we don't want vLLM's
CuMemAllocator to interfere.
"""
if tag == "weights":
logger.debug("[GMS] Skipping CuMemAllocator for weights")
return nullcontext()
return super()._maybe_get_memory_pool_context(tag)